├── tests ├── .gitignore ├── phpstan.neon ├── data │ ├── CA_TEST-01.p12 │ ├── EET_CA1_Playground-ca.crt │ ├── EET_CA1_Playground-CZ00000019.p12 │ ├── EET_CA1_Playground-CZ1212121218.p12 │ └── EET_CA1_Playground-CZ683555118.p12 ├── .coveralls.yml ├── php.ini ├── bootstrap.php └── cases │ ├── UtilsFileSystemTest.php │ ├── UtilsFormatTest.php │ ├── ReceiptTest.php │ ├── CertificateTest.php │ └── DispatcherTest.php ├── .gitignore ├── .editorconfig ├── .github └── FUNDING.yml ├── src ├── Exceptions │ ├── EetException.php │ ├── IOException.php │ ├── EET │ │ ├── EETException.php │ │ ├── ErrorException.php │ │ └── ClientException.php │ ├── RuntimeException.php │ ├── UnexpectedException.php │ ├── Receipt │ │ ├── ReceiptException.php │ │ └── ConstraintViolationException.php │ ├── FileSystem │ │ ├── FileSystemException.php │ │ └── FileNotFoundException.php │ ├── SoapClient │ │ ├── SoapClientException.php │ │ └── CurlException.php │ ├── Certificate │ │ ├── CertificateException.php │ │ ├── CertificateNotFoundException.php │ │ └── CertificateExportFailedException.php │ └── ExtensionNotFound.php ├── Utils │ ├── UUID.php │ ├── Format.php │ ├── FileSystem.php │ └── Debugger │ │ └── LastRequest.php ├── Enum │ ├── Warning.php │ └── Error.php ├── Schema │ ├── ProductionService.wsdl │ ├── PlaygroundService.wsdl │ └── EETXMLSchema.xsd ├── Certificate.php ├── Receipt.php ├── SoapClient.php └── Dispatcher.php ├── LICENSE ├── .travis.yml ├── README.md ├── composer.json ├── phpcs.xml └── .docs └── README.md /tests/.gitignore: -------------------------------------------------------------------------------- 1 | tmp -------------------------------------------------------------------------------- /tests/phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | /.idea 3 | 4 | # Composer 5 | /vendor 6 | /composer.lock -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [.travis.yml] 4 | indent_style = space 5 | indent_size = 4 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: filipsedivy 2 | custom: https://filipsedivy.cz/donation?to=PHP-EET -------------------------------------------------------------------------------- /tests/data/CA_TEST-01.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsedivy/php-eet/HEAD/tests/data/CA_TEST-01.p12 -------------------------------------------------------------------------------- /tests/data/EET_CA1_Playground-ca.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsedivy/php-eet/HEAD/tests/data/EET_CA1_Playground-ca.crt -------------------------------------------------------------------------------- /tests/.coveralls.yml: -------------------------------------------------------------------------------- 1 | # for php-coveralls 2 | service_name: travis-ci 3 | coverage_clover: coverage.xml 4 | json_path: coverage.json -------------------------------------------------------------------------------- /tests/data/EET_CA1_Playground-CZ00000019.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsedivy/php-eet/HEAD/tests/data/EET_CA1_Playground-CZ00000019.p12 -------------------------------------------------------------------------------- /tests/data/EET_CA1_Playground-CZ1212121218.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsedivy/php-eet/HEAD/tests/data/EET_CA1_Playground-CZ1212121218.p12 -------------------------------------------------------------------------------- /tests/data/EET_CA1_Playground-CZ683555118.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsedivy/php-eet/HEAD/tests/data/EET_CA1_Playground-CZ683555118.p12 -------------------------------------------------------------------------------- /src/Exceptions/EetException.php: -------------------------------------------------------------------------------- 1 | toString(); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Exceptions/ExtensionNotFound.php: -------------------------------------------------------------------------------- 1 | 'DIC poplatnika v datove zprave se neshoduje s DIC v certifikatu', 9 | 2 => 'Chybny format DIC poverujiciho poplatnika', 10 | 3 => 'Chybna hodnota PKP', 11 | 4 => 'Datum a cas prijeti trzby je novejsi nez datum a cas prijeti zpravy', 12 | 5 => 'Datum a cas prijeti trzby je vyrazne v minulosti ' 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /src/Exceptions/Certificate/CertificateNotFoundException.php: -------------------------------------------------------------------------------- 1 | run(); 23 | -------------------------------------------------------------------------------- /src/Enum/Error.php: -------------------------------------------------------------------------------- 1 | 'Docasna technicka chyba zpracovani – odeslete prosim datovou zpravu pozdeji', 9 | 2 => 'Kodovani XML neni platne', 10 | 3 => 'XML zprava nevyhovela kontrole XML schematu', 11 | 4 => 'Neplatny podpis SOAP zpravy', 12 | 5 => 'Neplatny kontrolni bezpecnostni kod poplatnika (BKP)', 13 | 6 => 'DIC poplatnika ma chybnou strukturu', 14 | 7 => 'Datova zprava je prilis velka', 15 | 8 => 'Datova zprava nebyla zpracovana kvuli technicke chybe nebo chybe dat', 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /src/Utils/Format.php: -------------------------------------------------------------------------------- 1 | path = $path; 16 | 17 | $message = "The certificate ('$path') cannot be exported"; 18 | parent::__construct($message, 0, $previous); 19 | } 20 | 21 | public function getPath(): string 22 | { 23 | return $this->path; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/cases/UtilsFormatTest.php: -------------------------------------------------------------------------------- 1 | run(); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Filip Šedivý 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Exceptions/EET/ClientException.php: -------------------------------------------------------------------------------- 1 | receipt = $receipt; 23 | $this->bkp = $bkp; 24 | $this->pkp = $pkp; 25 | 26 | parent::__construct($previous->getMessage(), $previous->getCode(), $previous); 27 | } 28 | 29 | public function getReceipt(): Receipt 30 | { 31 | return $this->receipt; 32 | } 33 | 34 | public function getBkp(): ?string 35 | { 36 | return $this->bkp; 37 | } 38 | 39 | public function getPkp(bool $encoded = true): ?string 40 | { 41 | $pkp = $this->pkp; 42 | 43 | if ($pkp === null) { 44 | return null; 45 | } 46 | 47 | if ($encoded) { 48 | $pkp = base64_encode($pkp); 49 | } 50 | 51 | return $pkp; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 7.1 4 | - 7.2 5 | - 7.3 6 | - 7.4 7 | - nightly 8 | 9 | env: 10 | - PHP_BIN=php 11 | - PHP_BIN=php-cgi 12 | 13 | before_script: 14 | - phpenv config-rm xdebug.ini || return 0 15 | - travis_retry composer self-update 16 | 17 | install: 18 | - travis_retry composer install --no-interaction --no-progress --prefer-dist 19 | 20 | script: 21 | - ./vendor/bin/nunjuck -p $PHP_BIN -s -c ./tests/php.ini ./tests 22 | 23 | after_failure: 24 | - for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done 25 | 26 | jobs: 27 | include: 28 | - name: Lowest Dependencies 29 | env: PHP_BIN=php 30 | php: 7.2 31 | install: 32 | - travis_retry composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable 33 | 34 | 35 | - stage: Code Sniffer 36 | php: 7.3 37 | script: 38 | - vendor/bin/phpcs --standard=phpcs.xml --encoding=utf-8 -sp src/ tests/ 39 | 40 | 41 | - stage: Static Analysis 42 | php: 7.3 43 | script: 44 | - travis_retry composer phpstan 45 | 46 | 47 | - stage: Code Coverage 48 | php: 7.3 49 | script: 50 | - travis_retry composer coverage 51 | after_script: 52 | - wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar 53 | - php coveralls.phar --verbose --config tests/.coveralls.yml 54 | 55 | allow_failures: 56 | - stage: Code Coverage 57 | - php: 7.4 58 | - php: nightly 59 | 60 | cache: 61 | directories: 62 | - "$HOME/.composer/cache" 63 | 64 | notifications: 65 | email: false -------------------------------------------------------------------------------- /src/Exceptions/Receipt/ConstraintViolationException.php: -------------------------------------------------------------------------------- 1 | constraintViolationList = $constraintViolationList; 23 | 24 | $errors = []; 25 | $properties = []; 26 | 27 | foreach ($constraintViolationList as $violation) { 28 | if ($violation instanceof ConstraintViolationInterface) { 29 | $properties[] = $violation->getPropertyPath(); 30 | $errors[] = sprintf('[%s] %s', $violation->getPropertyPath(), $violation->getMessage()); 31 | } 32 | } 33 | 34 | $this->errors = $errors; 35 | $this->properties = $properties; 36 | 37 | parent::__construct('Incorrect value in properties: ' . implode(', ', $properties), 0, null); 38 | } 39 | 40 | public function getConstraintViolationList(): ConstraintViolationListInterface 41 | { 42 | return $this->constraintViolationList; 43 | } 44 | 45 | public function getErrors(): array 46 | { 47 | return $this->errors; 48 | } 49 | 50 | public function getProperties(): array 51 | { 52 | return $this->properties; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Client for electronic records of sale

2 | 3 |

4 | Powerful & effective ⚡️ PHP library for electronic records of sale 5 |

6 | 7 |

8 | Contact 🚀 filipsedivy.cz | Twitter 🐦 @filipsedivy 9 |

10 | 11 |

12 | 13 | 14 | 15 | 16 | 17 |

18 | 19 |

20 | ✏️ There are official implementations for frameworks. 21 |

22 | 23 |

24 | 25 |

26 | 27 | ----- 28 | 29 | ## Usage 30 | 31 | To install latest version of `filipsedivy/php-eet` use [Composer](https://getcomposer.com). 32 | 33 | ```bash 34 | composer require filipsedivy/php-eet 35 | ``` 36 | 37 | ## Documentation 38 | 39 | For detailed instructions on how to use the library, visit the [documentation](.docs/README.md). 40 | 41 | ## Contributing 42 | 43 | php-eet is an Open Source, community-driven project. You can help develop code or create documentation. 44 | 45 | ----- 46 | 47 | If the library is useful, **[please make a donation now](https://filipsedivy.cz/donation?to=php-eet)**. Thank you! -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filipsedivy/php-eet", 3 | "description": "💸 Client for electronic records of sales", 4 | "homepage": "https://github.com/filipsedivy/PHP-EET", 5 | "license": [ 6 | "MIT" 7 | ], 8 | "keywords": [ 9 | "EET", 10 | "elektronická evidence tržeb", 11 | "etrzby.cz" 12 | ], 13 | "type": "library", 14 | "support": { 15 | "email": "mail@filipsedivy.cz", 16 | "issues": "https://github.com/filipsedivy/PHP-EET/issues", 17 | "source:": "https://github.com/filipsedivy/PHP-EET" 18 | }, 19 | "authors": [ 20 | { 21 | "name": "Filip Šedivý", 22 | "email": "mail@filipsedivy.cz", 23 | "homepage": "https://filipsedivy.cz", 24 | "role": "Developer" 25 | } 26 | ], 27 | "require": { 28 | "php": ">=7.1", 29 | "ext-curl": "*", 30 | "ext-dom": "*", 31 | "ext-openssl": "*", 32 | "ext-soap": "*", 33 | "ramsey/uuid": "^3.9 || ^4.0", 34 | "robrichards/wse-php": "^2.0", 35 | "symfony/validator": "^4.4 || ^5.0" 36 | }, 37 | "require-dev": { 38 | "ninjify/nunjuck": "^0.2", 39 | "phpstan/phpstan": "^0.8", 40 | "slevomat/coding-standard": "^5.0", 41 | "squizlabs/php_codesniffer": "^3.4" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "FilipSedivy\\EET\\": "src" 46 | } 47 | }, 48 | "minimum-stability": "dev", 49 | "prefer-stable": true, 50 | "extra": { 51 | "branch-alias": { 52 | "dev-master": "4.2-dev" 53 | } 54 | }, 55 | "scripts": { 56 | "full": [ 57 | "@cs", 58 | "@phpstan", 59 | "@tester" 60 | ], 61 | "phpstan": "phpstan analyse --level 7 --configuration tests/phpstan.neon src/", 62 | "cs": "phpcs --standard=phpcs.xml --encoding=utf-8 -sp src/ tests/cases/", 63 | "cbf": "phpcbf --standard=phpcs.xml --colors --encoding=utf-8 -nsp src/ tests/", 64 | "tester": "nunjuck -s -c ./tests/php.ini ./tests", 65 | "coverage": "nunjuck -p phpdbg tests -s --coverage ./coverage.xml --coverage-src ./src" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/cases/ReceiptTest.php: -------------------------------------------------------------------------------- 1 | uuid_zpravy = '8f5138bf-49e2-4ee9-9509-d75d01095609'; 19 | 20 | $header = [ 21 | 'uuid_zpravy' => '8f5138bf-49e2-4ee9-9509-d75d01095609', 22 | 'prvni_zaslani' => true 23 | ]; 24 | 25 | Assert::same($receipt->buildHeader(), $header); 26 | } 27 | 28 | public function testSendEmptyReceipt(): void 29 | { 30 | $certificate = EET\Certificate::fromFile(DATA_DIR . '/EET_CA1_Playground-CZ00000019.p12', 'eet'); 31 | $dispatcher = new EET\Dispatcher($certificate, EET\Dispatcher::PLAYGROUND_SERVICE); 32 | 33 | Assert::exception(static function () use ($dispatcher) { 34 | $receipt = new EET\Receipt; 35 | $dispatcher->send($receipt); 36 | }, EET\Exceptions\Receipt\ConstraintViolationException::class); 37 | } 38 | 39 | public function testEmptyCodes(): void 40 | { 41 | $receipt = new EET\Receipt; 42 | $exception = new EET\Exceptions\EET\ClientException($receipt, null, null, new Exception); 43 | 44 | Assert::null($exception->getPkp()); 45 | Assert::null($exception->getBkp()); 46 | } 47 | 48 | public function testConstraintViolation(): void 49 | { 50 | $certificate = EET\Certificate::fromFile(DATA_DIR . '/EET_CA1_Playground-CZ00000019.p12', 'eet'); 51 | $dispatcher = new EET\Dispatcher($certificate, EET\Dispatcher::PLAYGROUND_SERVICE, true); 52 | 53 | $receipt = new EET\Receipt; 54 | $receipt->dic_popl = 'BadValue'; 55 | 56 | try { 57 | $dispatcher->getCheckCodes($receipt); 58 | } catch (EET\Exceptions\Receipt\ConstraintViolationException $exception) { 59 | Assert::type('array', $exception->getErrors()); 60 | Assert::type('array', $exception->getProperties()); 61 | Assert::type(Validator\ConstraintViolationListInterface::class, $exception->getConstraintViolationList()); 62 | } 63 | } 64 | } 65 | 66 | (new ReceiptTest)->run(); 67 | -------------------------------------------------------------------------------- /src/Schema/ProductionService.wsdl: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | Ucel : Sluzba pro odeslani datove zpravy evidovane trzby 17 | Verze : 3.1 18 | Vlastnik : Generalni financni reditelstvi 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/Schema/PlaygroundService.wsdl: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | Ucel : Sluzba pro odeslani datove zpravy evidovane trzby 17 | Verze : 3.1 18 | Vlastnik : Generalni financni reditelstvi 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/Utils/Debugger/LastRequest.php: -------------------------------------------------------------------------------- 1 | lastRequest = $lastRequest; 33 | } 34 | 35 | public function out(): void 36 | { 37 | if ($this->hiddenSensitiveData) { 38 | $this->doHiddenSensitiveData(); 39 | } 40 | 41 | if ($this->format) { 42 | $this->doFormat(); 43 | } 44 | 45 | if ($this->highlight) { 46 | $this->doHighlight(); 47 | } 48 | 49 | printf('
%s
', $this->lastRequest); 50 | } 51 | 52 | private function doFormat(): void 53 | { 54 | $dom = new DOMDocument; 55 | 56 | $dom->preserveWhiteSpace = false; 57 | $dom->formatOutput = true; 58 | 59 | $dom->loadXML($this->lastRequest); 60 | 61 | $this->lastRequest = $dom->saveXML(); 62 | } 63 | 64 | private function doHighlight(): void 65 | { 66 | $s = htmlspecialchars($this->lastRequest); 67 | 68 | $s = preg_replace("#<([/]*?)(.*)([\s]*?)>#sU", 69 | "<\\1\\2\\3>", $s); 70 | 71 | $s = preg_replace("#<([\?])(.*)([\?])>#sU", 72 | "<\\1\\2\\3>", $s); 73 | 74 | $s = preg_replace("#<([^\s\?/=])(.*)([\[\s/]|>)#iU", 75 | "<\\1\\2\\3", $s); 76 | 77 | $s = preg_replace("#<([/])([^\s]*?)([\s\]]*?)>#iU", 78 | "<\\1\\2\\3>", $s); 79 | 80 | $s = preg_replace("#([^\s]*?)\=("|')(.*)("|')#isU", 81 | "\\1=\\2\\3\\4", $s); 82 | 83 | $s = preg_replace("#<(.*)(\[)(.*)(\])>#isU", 84 | "<\\1\\2\\3\\4>", $s); 85 | 86 | $this->lastRequest = nl2br($s); 87 | } 88 | 89 | private function doHiddenSensitiveData(): void 90 | { 91 | $dom = new DOMDocument; 92 | $dom->loadXML($this->lastRequest); 93 | 94 | foreach (self::SENSITIVE_TAGS as $tag) { 95 | $nodeList = $dom->getElementsByTagName($tag); 96 | 97 | for ($i = 0; $i < $nodeList->length; $i++) { 98 | $node = $nodeList->item($i); 99 | $node->nodeValue = $this->sensitiveValue; 100 | } 101 | } 102 | 103 | $this->lastRequest = $dom->saveXML(); 104 | } 105 | } -------------------------------------------------------------------------------- /tests/cases/CertificateTest.php: -------------------------------------------------------------------------------- 1 | getPrivateKey()); 37 | Assert::type('string', $certificate->getCertificate()); 38 | } 39 | 40 | 41 | public function testCertificateFromFile(): void 42 | { 43 | $certificate = Certificate::fromFile(DATA_DIR . '/EET_CA1_Playground-CZ00000019.p12', 'eet'); 44 | 45 | Assert::type('string', $certificate->getPrivateKey()); 46 | Assert::type('string', $certificate->getCertificate()); 47 | } 48 | 49 | public function testBadPassword(): void 50 | { 51 | Assert::exception(static function () { 52 | Certificate::fromFile(DATA_DIR . '/EET_CA1_Playground-CZ00000019.p12', 'password'); 53 | }, Exceptions\Certificate\CertificateExportFailedException::class); 54 | } 55 | 56 | public function testCertificatePath(): void 57 | { 58 | try { 59 | Certificate::fromFile(DATA_DIR . '/EET_CA1_Playground-CZ00000019.p12', 'password'); 60 | 61 | Assert::fail('Certificate have bad password'); 62 | } catch (Exceptions\Certificate\CertificateExportFailedException $exception) { 63 | Assert::type('string', $exception->getPath()); 64 | } 65 | } 66 | 67 | public function testCertificateValidation(): void 68 | { 69 | $certificate = Certificate::fromFile(DATA_DIR . '/EET_CA1_Playground-CZ00000019.p12', 'eet'); 70 | $certificate2 = Certificate::fromFile(DATA_DIR . '/CA_TEST-01.p12', 'test'); 71 | 72 | Assert::true($certificate->isIssuerOk()); 73 | Assert::true($certificate->isValidOk()); 74 | Assert::true($certificate->isCertificateOk()); 75 | Assert::true($certificate->isOk()); 76 | 77 | Assert::false($certificate2->isIssuerOk()); 78 | Assert::true($certificate2->isValidOk()); 79 | Assert::true($certificate2->isCertificateOk()); 80 | Assert::false($certificate2->isOk()); 81 | } 82 | 83 | public function testExport(): void 84 | { 85 | $certificate = Certificate::fromFile(DATA_DIR . '/EET_CA1_Playground-CZ00000019.p12', 'eet'); 86 | 87 | Assert::type('array', $certificate->getExport()); 88 | Assert::type('array', $certificate->getIssuer()); 89 | Assert::type('array', $certificate->getSubject()); 90 | 91 | Assert::type('bool', $certificate->isOk()); 92 | Assert::type('bool', $certificate->isValidOk()); 93 | Assert::type('bool', $certificate->isIssuerOk()); 94 | Assert::type('bool', $certificate->isCertificateOk()); 95 | 96 | Assert::type(DateTime::class, $certificate->getValidFrom()); 97 | Assert::type(DateTime::class, $certificate->getValidTo()); 98 | } 99 | } 100 | 101 | (new CertificateTest)->run(); 102 | -------------------------------------------------------------------------------- /src/Certificate.php: -------------------------------------------------------------------------------- 1 | privateKey = $privateKey; 59 | $this->certificate = $certificate; 60 | $this->export = $export; 61 | } 62 | 63 | public function getPrivateKey(): string 64 | { 65 | return $this->privateKey; 66 | } 67 | 68 | public function getCertificate(): string 69 | { 70 | return $this->certificate; 71 | } 72 | 73 | public function getExport(): array 74 | { 75 | return $this->export; 76 | } 77 | 78 | public function getIssuer(): array 79 | { 80 | $export = $this->getExport(); 81 | 82 | return $export['issuer']; 83 | } 84 | 85 | public function getSubject(): array 86 | { 87 | $export = $this->getExport(); 88 | 89 | return $export['subject']; 90 | } 91 | 92 | public function getValidFrom(): DateTime 93 | { 94 | $export = $this->getExport(); 95 | 96 | return date_create_from_format('ymdHise', $export['validFrom']); 97 | } 98 | 99 | public function getValidTo(): DateTime 100 | { 101 | $export = $this->getExport(); 102 | 103 | return date_create_from_format('ymdHise', $export['validTo']); 104 | } 105 | 106 | public function isIssuerOk(): bool 107 | { 108 | $production = [ 109 | 'DC' => 'CZ', 110 | 'O' => 'Česká Republika – Generální finanční ředitelství', 111 | 'CN' => 'EET CA 1' 112 | ]; 113 | 114 | $playground = [ 115 | 'DC' => 'CZ', 116 | 'O' => 'Česká Republika – Generální finanční ředitelství', 117 | 'CN' => 'EET CA 1 Playground' 118 | ]; 119 | 120 | return count(array_diff_assoc($this->getIssuer(), $production, $playground)) === 0; 121 | } 122 | 123 | public function isCertificateOk(): bool 124 | { 125 | $export = $this->getExport(); 126 | 127 | return $export['signatureTypeSN'] === 'RSA-SHA256'; 128 | } 129 | 130 | public function isValidOk(): bool 131 | { 132 | $now = new DateTime('now'); 133 | 134 | return ($this->getValidTo() > $now) && ($this->getValidFrom() < $now); 135 | } 136 | 137 | public function isOk(): bool 138 | { 139 | return $this->isIssuerOk() && 140 | $this->isCertificateOk() && 141 | $this->isValidOk(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /.docs/README.md: -------------------------------------------------------------------------------- 1 | # Client for electronic records of sale 2 | 3 | ## Usage 4 | 5 | ```php 6 | uuid_zpravy = Uuid::uuid4()->toString(); 14 | $receipt->id_provoz = '141'; 15 | $receipt->id_pokl = '1patro-vpravo'; 16 | $receipt->porad_cis = '141-18543-05'; 17 | $receipt->dic_popl = 'CZ00000019'; 18 | $receipt->dat_trzby = new DateTime; 19 | $receipt->celk_trzba = 500; 20 | 21 | $certificate = Certificate::fromFile('EET_CA1_Playground-CZ00000019.p12', 'eet'); 22 | $dispatcher = new Dispatcher($certificate, Dispatcher::PLAYGROUND_SERVICE); 23 | 24 | try { 25 | $dispatcher->send($receipt); 26 | 27 | echo 'FIK: ' . $dispatcher->getFik(); 28 | echo 'BKP: ' . $dispatcher->getBkp(); 29 | } catch (FilipSedivy\EET\Exceptions\EET\ClientException $exception) { 30 | echo 'BKP: ' . $exception->getBkp(); 31 | echo 'PKP:' . $exception->getPkp(); 32 | } catch (FilipSedivy\EET\Exceptions\EET\ErrorException $exception) { 33 | echo '(' . $exception->getCode() . ') ' . $exception->getMessage(); 34 | } catch (FilipSedivy\EET\Exceptions\Receipt\ConstraintViolationException $violationException) { 35 | echo implode('
', $violationException->getErrors()); 36 | } 37 | ``` 38 | 39 | ## Certificate 40 | 41 | These are classes that allow the certificate to be exported without further processing. 42 | 43 | Received file in p12 format, the one that etrzby.cz will export. 44 | 45 | ```php 46 | $certificate = FilipSedivy\EET\Certificate(string $file, string $password); 47 | ``` 48 | 49 | **Parameters:** 50 | - `string $file`: File path 51 | - `string $password`: Certificate password 52 | 53 | ## Dispatcher 54 | 55 | Dispatcher is a class that takes care of recipe validation and communication with SoapClient. 56 | 57 | ```php 58 | $dispatcher = FilipSedivy\EET\Dispatcher(Certificate $certificate, ?string $service = self::PRODUCTION_SERVICE, bool $validate = true); 59 | ``` 60 | 61 | **Parameters:** 62 | - `Certificate $certificate`: Certificate class instance 63 | - `?string $service`: Setting services (`self::PLAYGROUND_SERVICE` OR `self::PRODUCTION_SERVICE`) 64 | - `bool $validate`: Enable offline Receipt validation 65 | 66 | **Methods:** 67 | - `check(Receipt $receipt): bool`: Receipt verification without registering EET 68 | - `send(Receipt $receipt, bool $check = false): ?string`: Send EET to the server. If everything is OK, the string with FIK is returned. If `$check = true` is enabled, FIK is not returned 69 | 70 | ## Exceptions 71 | 72 | All exceptions have a common namespace `FilipSedivy\EET\Exceptions` 73 | 74 | ### EET\ClientException 75 | 76 | This is an exception that is thrown when there is a problem communicating with the target server. 77 | 78 | **This exception allows you to get the latest BKP, PKP and receipt.** 79 | 80 | ```php 81 | try { 82 | $dispatcher->send($receipt); 83 | } catch (FilipSedivy\EET\Exceptions\EET\ClientException $exception) { 84 | echo 'BKP: ' . $exception->getBkp(); 85 | echo 'PKP:' . $exception->getPkp(); 86 | print_r($exception->getReceipt()); 87 | } 88 | ``` 89 | 90 | ### EET\ErrorException 91 | 92 | This error occurs when an error is returned directly from the EET server. In this case, the entire message is invalid and not recorded. For this reason it is not possible to get PKP and BKP code from the exception. 93 | 94 | `getCode()` returns the error code from the target server. `getMessage()` returns the translated error according to etrzby.cz documentation. 95 | 96 | ```php 97 | try { 98 | $dispatcher->send($receipt); 99 | } catch (FilipSedivy\EET\Exceptions\EET\ErrorException $exception) { 100 | echo '(' . $exception->getCode() . ') ' . $exception->getMessage(); 101 | } 102 | ``` 103 | 104 | ### Receipt\ConstraintViolationException 105 | 106 | If validation is enabled, this exception is thrown in case of invalid value according to the scheme. 107 | 108 | In this case, the EET is not sent to the destination server and the Receipt is invalid and it is not possible to obtain BKP and PKP codes. 109 | 110 | ```php 111 | try { 112 | $dispatcher->send($receipt); 113 | } catch (FilipSedivy\EET\Exceptions\Receipt\ConstraintViolationException $violationException) { 114 | echo implode('
', $violationException->getErrors()); 115 | } 116 | ``` -------------------------------------------------------------------------------- /tests/cases/DispatcherTest.php: -------------------------------------------------------------------------------- 1 | certificate = EET\Certificate::fromFile(DATA_DIR . '/EET_CA1_Playground-CZ00000019.p12', 'eet'); 21 | } 22 | 23 | public function testService(): void 24 | { 25 | $dispatcher = new EET\Dispatcher($this->certificate, 'Personal/MySchema/MyService.wsdl'); 26 | 27 | Assert::contains('Personal/MySchema/MyService.wsdl', $dispatcher->getService()); 28 | 29 | $schemaPath = 'Schema'; 30 | 31 | $dispatcher->setProductionService(); 32 | 33 | Assert::contains($schemaPath . '/ProductionService.wsdl', $dispatcher->getService()); 34 | 35 | $dispatcher->setPlaygroundService(); 36 | 37 | Assert::contains($schemaPath . '/PlaygroundService.wsdl', $dispatcher->getService()); 38 | 39 | $dispatcher->setService('Personal/MySchema/MyService.wsdl'); 40 | 41 | Assert::contains('Personal/MySchema/MyService.wsdl', $dispatcher->getService()); 42 | 43 | $dispatcher = new EET\Dispatcher($this->certificate, EET\Dispatcher::PRODUCTION_SERVICE); 44 | 45 | Assert::contains($schemaPath . '/ProductionService.wsdl', $dispatcher->getService()); 46 | } 47 | 48 | public function testSendReceipt(): void 49 | { 50 | $dispatcher = new EET\Dispatcher($this->certificate, EET\Dispatcher::PLAYGROUND_SERVICE); 51 | 52 | $dispatcher->send($this->getValidReceipt()); 53 | 54 | Assert::type('string', $dispatcher->getFik()); 55 | Assert::type('string', $dispatcher->getBkp()); 56 | Assert::type('string', $dispatcher->getSoapClient()->lastResponse); 57 | Assert::same(200, $dispatcher->getSoapClient()->getLastResponseHttpCode()); 58 | Assert::type(DateTime::class, $dispatcher->getSentDateTime()); 59 | } 60 | 61 | public function testFailed(): void 62 | { 63 | static $proxy = ['127.0.0.1', 8888]; 64 | $dispatcher = new EET\Dispatcher($this->certificate, EET\Dispatcher::PLAYGROUND_SERVICE); 65 | $dispatcher->setCurlOption(CURLOPT_PROXY, implode(':', $proxy)); 66 | 67 | Assert::exception(function () use ($dispatcher) { 68 | $dispatcher->send($this->getValidReceipt()); 69 | }, EET\Exceptions\EET\ClientException::class); 70 | 71 | try { 72 | $dispatcher->send($this->getValidReceipt()); 73 | } catch (EET\Exceptions\EET\ClientException $client) { 74 | Assert::type('string', $client->getBkp()); 75 | Assert::type('string', $client->getPkp()); 76 | Assert::null($dispatcher->getSoapClient()->getLastResponseHttpCode()); 77 | Assert::type(DateTime::class, $dispatcher->getSentDateTime()); 78 | 79 | if (!$client->getReceipt() instanceof EET\Receipt) { 80 | Assert::fail('Client->getReceipt() is not instanceof Receipt'); 81 | } 82 | } 83 | } 84 | 85 | public function testCheck(): void 86 | { 87 | $dispatcher = new EET\Dispatcher($this->certificate, EET\Dispatcher::PLAYGROUND_SERVICE); 88 | 89 | $receipt = $this->getValidReceipt(); 90 | Assert::true($dispatcher->check($receipt)); 91 | 92 | $receipt->dic_popl = 'CZ00000018'; 93 | Assert::false($dispatcher->check($receipt)); 94 | } 95 | 96 | public function testGetCheckCodes(): void 97 | { 98 | $dispatcher = new EET\Dispatcher($this->certificate, EET\Dispatcher::PLAYGROUND_SERVICE); 99 | 100 | $receipt = $this->getValidReceipt(); 101 | 102 | Assert::type('array', $dispatcher->getCheckCodes($receipt)); 103 | 104 | $receipt->bkp = $dispatcher->getBkp(); 105 | $receipt->pkp = $dispatcher->getPkp(false); 106 | 107 | Assert::type('array', $dispatcher->getCheckCodes($receipt)); 108 | } 109 | 110 | public function testLastReceipt(): void 111 | { 112 | $dispatcher = new EET\Dispatcher($this->certificate, EET\Dispatcher::PLAYGROUND_SERVICE); 113 | 114 | Assert::null($dispatcher->getLastReceipt()); 115 | 116 | $dispatcher->send($this->getValidReceipt()); 117 | 118 | Assert::type(EET\Receipt::class, $dispatcher->getLastReceipt()); 119 | } 120 | 121 | public function testGetWarnings(): void 122 | { 123 | $dispatcher = new EET\Dispatcher($this->certificate, EET\Dispatcher::PLAYGROUND_SERVICE); 124 | 125 | Assert::type('array', $dispatcher->getWarnings()); 126 | Assert::count(0, $dispatcher->getWarnings()); 127 | } 128 | 129 | public function testGetPkp(): void 130 | { 131 | $dispatcher = new EET\Dispatcher($this->certificate, EET\Dispatcher::PLAYGROUND_SERVICE); 132 | 133 | Assert::null($dispatcher->getPkp()); 134 | 135 | $dispatcher->send($this->getValidReceipt()); 136 | 137 | Assert::type('string', $dispatcher->getPkp()); 138 | } 139 | 140 | public function testGetSentDateTime(): void 141 | { 142 | $dispatcher = new EET\Dispatcher($this->certificate, EET\Dispatcher::PLAYGROUND_SERVICE); 143 | 144 | Assert::null($dispatcher->getSentDateTime()); 145 | 146 | $dispatcher->send($this->getValidReceipt()); 147 | 148 | Assert::type(DateTime::class, $dispatcher->getSentDateTime()); 149 | } 150 | 151 | public function testGetSoapClient(): void 152 | { 153 | $dispatcher = new EET\Dispatcher($this->certificate, EET\Dispatcher::PLAYGROUND_SERVICE); 154 | 155 | Assert::type(EET\SoapClient::class, $dispatcher->getSoapClient()); 156 | } 157 | 158 | private function getValidReceipt(): EET\Receipt 159 | { 160 | $receipt = new EET\Receipt; 161 | $receipt->uuid_zpravy = Uuid::uuid4()->toString(); 162 | $receipt->id_provoz = '11'; 163 | $receipt->id_pokl = 'IP105'; 164 | $receipt->dic_popl = 'CZ00000019'; 165 | $receipt->porad_cis = '1'; 166 | $receipt->dat_trzby = new DateTime; 167 | $receipt->celk_trzba = 500; 168 | 169 | return $receipt; 170 | } 171 | } 172 | 173 | (new DispatcherTest)->run(); 174 | -------------------------------------------------------------------------------- /src/Receipt.php: -------------------------------------------------------------------------------- 1 | {$parameter}; 112 | 113 | $header[$parameter] = $value; 114 | } 115 | 116 | return $header; 117 | } 118 | 119 | public function buildBody(bool $autoFormatPrice = true): array 120 | { 121 | $body = []; 122 | 123 | // build require parameters 124 | foreach (self::BODY_REQUIRE as $parameter) { 125 | $value = $this->{$parameter}; 126 | 127 | if ($value instanceof DateTime) { 128 | $value = $value->format('c'); 129 | } 130 | 131 | $body[$parameter] = $value; 132 | } 133 | 134 | // build optional parameters 135 | foreach (self::BODY_OPTIONAL as $parameter) { 136 | $value = $this->{$parameter}; 137 | 138 | if ($value !== null) { 139 | $body[$parameter] = $value; 140 | } 141 | } 142 | 143 | // format price 144 | if ($autoFormatPrice) { 145 | foreach (self::BODY_PRICE_FORMAT as $item) { 146 | if (array_key_exists($item, $body) && $body[$item] !== null) { 147 | $body[$item] = Format::price($body[$item]); 148 | } 149 | } 150 | } 151 | 152 | return $body; 153 | } 154 | 155 | public static function loadValidatorMetadata(ClassMetadata $metadata): void 156 | { 157 | $metadata 158 | ->addPropertyConstraint('uuid_zpravy', new Assert\NotBlank) 159 | ->addPropertyConstraint('uuid_zpravy', new Assert\Type('string')) 160 | ->addPropertyConstraint('uuid_zpravy', new Assert\Uuid([ 161 | 'versions' => [Assert\Uuid::V4_RANDOM] 162 | ])); 163 | 164 | $metadata 165 | ->addPropertyConstraint('prvni_zaslani', new Assert\Type('bool')); 166 | 167 | $metadata 168 | ->addPropertyConstraint('dic_popl', new Assert\NotBlank) 169 | ->addPropertyConstraint('dic_popl', new Assert\Regex([ 170 | 'pattern' => '/^CZ([0-9]{8,10})$/' 171 | ])); 172 | 173 | $metadata 174 | ->addPropertyConstraint('dic_poverujiciho', new Assert\Regex([ 175 | 'pattern' => '/^CZ([0-9]{8,10})$/' 176 | ])); 177 | 178 | $metadata 179 | ->addPropertyConstraint('id_provoz', new Assert\NotBlank) 180 | ->addPropertyConstraint('id_provoz', new Assert\Regex([ 181 | 'pattern' => '/^[1-9][0-9]{0,5}$/' 182 | ])); 183 | 184 | $metadata 185 | ->addPropertyConstraint('id_pokl', new Assert\NotBlank) 186 | ->addPropertyConstraint('id_pokl', new Assert\Regex([ 187 | 'pattern' => '/^[0-9a-zA-Z\.,:;\/#\-_ ]{1,20}$/' 188 | ])); 189 | 190 | $metadata 191 | ->addPropertyConstraint('porad_cis', new Assert\NotBlank) 192 | ->addPropertyConstraint('porad_cis', new Assert\Regex([ 193 | 'pattern' => '/^[0-9a-zA-Z\.,:;\/#\-_ ]{1,25}$/' 194 | ])); 195 | 196 | $metadata 197 | ->addPropertyConstraint('dat_trzby', new Assert\NotBlank) 198 | ->addPropertyConstraint('dat_trzby', new Assert\Type(DateTime::class)); 199 | 200 | $metadata 201 | ->addPropertyConstraint('celk_trzba', new Assert\NotBlank) 202 | ->addPropertyConstraint('celk_trzba', new Assert\Type('numeric')); 203 | 204 | $metadata 205 | ->addPropertyConstraint('zakl_nepodl_dph', new Assert\Type('numeric')); 206 | 207 | $metadata 208 | ->addPropertyConstraint('zakl_dan1', new Assert\Type('numeric')); 209 | 210 | $metadata 211 | ->addPropertyConstraint('dan1', new Assert\Type('numeric')); 212 | 213 | $metadata 214 | ->addPropertyConstraint('zakl_dan2', new Assert\Type('numeric')); 215 | 216 | $metadata 217 | ->addPropertyConstraint('dan2', new Assert\Type('numeric')); 218 | 219 | $metadata 220 | ->addPropertyConstraint('zakl_dan3', new Assert\Type('numeric')); 221 | 222 | $metadata 223 | ->addPropertyConstraint('dan3', new Assert\Type('numeric')); 224 | 225 | $metadata 226 | ->addPropertyConstraint('cest_sluz', new Assert\Type('numeric')); 227 | 228 | $metadata 229 | ->addPropertyConstraint('pouzit_zboz1', new Assert\Type('numeric')); 230 | 231 | $metadata 232 | ->addPropertyConstraint('pouzit_zboz2', new Assert\Type('numeric')); 233 | 234 | $metadata 235 | ->addPropertyConstraint('pouzit_zboz3', new Assert\Type('numeric')); 236 | 237 | $metadata 238 | ->addPropertyConstraint('urceno_cerp_zuct', new Assert\Type('numeric')); 239 | 240 | $metadata 241 | ->addPropertyConstraint('cerp_zuct', new Assert\Type('numeric')); 242 | 243 | $metadata 244 | ->addPropertyConstraint('rezim', new Assert\NotBlank) 245 | ->addPropertyConstraint('rezim', new Assert\Regex([ 246 | 'pattern' => '/^[01]$/' 247 | ])); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/SoapClient.php: -------------------------------------------------------------------------------- 1 | true, 56 | 'trace' => $trace 57 | ]); 58 | 59 | $this->certificate = $certificate; 60 | $this->trace = $trace; 61 | $this->curlOptions = $curlOptions; 62 | } 63 | 64 | public function getXML($request) 65 | { 66 | $doc = new \DOMDocument('1.0'); 67 | $doc->loadXML($request); 68 | 69 | $objWSSE = new WSSESoap($doc); 70 | $objWSSE->addTimestamp(); 71 | 72 | $objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']); 73 | $objKey->loadKey($this->certificate->getPrivateKey()); 74 | $objWSSE->signSoapDoc($objKey, ['algorithm' => XMLSecurityDSig::SHA256]); 75 | 76 | $token = $objWSSE->addBinaryToken($this->certificate->getCertificate()); 77 | $objWSSE->attachTokentoSig($token); 78 | 79 | return $objWSSE->saveXML(); 80 | } 81 | 82 | public function __doRequest($request, $location, $action, $version, $one_way = 0): ?string 83 | { 84 | $xml = $this->getXML($request); 85 | $this->lastRequest = $xml; 86 | 87 | if ($this->returnRequest) { 88 | return ''; 89 | } 90 | 91 | $this->trace && $this->lastResponseStartTime = microtime(true); 92 | 93 | $this->lastResponse = $this->doRequestByCurl($xml, $location, $action, $version, $one_way); 94 | 95 | $this->trace && $this->lastResponseEndTime = microtime(true); 96 | 97 | return $this->lastResponse; 98 | } 99 | 100 | public function doRequestByCurl(string $request, string $location, string $action, int $version, int $one_way = 0): ?string 101 | { 102 | $this->lastResponseHttpCode = null; 103 | 104 | $curl = curl_init($location); 105 | 106 | if ($curl === false) { 107 | throw new CurlException('Curl initialisation failed'); 108 | } 109 | 110 | $headers = array( 111 | 'User-Agent: PHP-SOAP', 112 | 'Content-Type: text/xml; charset=utf-8', 113 | 'SOAPAction: "' . $action . '"', 114 | 'Content-Length: ' . strlen($request), 115 | ); 116 | 117 | $options = array( 118 | CURLOPT_VERBOSE => false, 119 | CURLOPT_RETURNTRANSFER => true, 120 | CURLOPT_POST => true, 121 | CURLOPT_POSTFIELDS => $request, 122 | CURLOPT_HEADER => $headers, 123 | CURLOPT_HTTPHEADER => [ 124 | sprintf('Content-Type: %s', $version === 2 ? 'application/soap+xml' : 'text/xml'), 125 | sprintf('SOAPAction: %s', $action) 126 | ], 127 | ); 128 | 129 | $options = array_replace($options, $this->curlOptions); 130 | 131 | $options = $this->curlSetTimeoutOption($options, $this->timeout, 'CURLOPT_TIMEOUT'); 132 | $options = $this->curlSetTimeoutOption($options, $this->connectTimeout, 'CURLOPT_CONNECTTIMEOUT'); 133 | 134 | $this->setCurlOptions($curl, $options); 135 | $response = curl_exec($curl); 136 | 137 | if (curl_errno($curl)) { 138 | $errorMessage = curl_error($curl); 139 | $errorNumber = curl_errno($curl); 140 | curl_close($curl); 141 | 142 | throw new CurlException($errorMessage, $errorNumber); 143 | } 144 | 145 | $this->lastResponseHttpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); 146 | 147 | $header_len = curl_getinfo($curl, CURLINFO_HEADER_SIZE); 148 | $body = substr($response, $header_len); 149 | 150 | curl_close($curl); 151 | 152 | return $one_way ? null : $body; 153 | } 154 | 155 | private function setCurlOptions($curl, array $options): void 156 | { 157 | foreach ($options as $option => $value) { 158 | if (curl_setopt($curl, $option, $value) !== false) { 159 | continue; 160 | } 161 | 162 | $export = var_export($value, true); 163 | 164 | throw new CurlException(sprintf('Failed setting CURL option %d to %s', $option, $export)); 165 | } 166 | } 167 | 168 | private function curlSetTimeoutOption($options, $milliseconds, $name) 169 | { 170 | if ($milliseconds > 0) { 171 | if (defined("{$name}_MS")) { 172 | $options[constant("{$name}_MS")] = $milliseconds; 173 | } else { 174 | $seconds = ceil($milliseconds / 1000); 175 | $options[$name] = $seconds; 176 | } 177 | 178 | if ($milliseconds <= 1000) { 179 | $options[CURLOPT_NOSIGNAL] = 1; 180 | } 181 | } 182 | 183 | return $options; 184 | } 185 | 186 | public function getLastResponseTime(): float 187 | { 188 | return $this->lastResponseEndTime - $this->lastResponseStartTime; 189 | } 190 | 191 | public function getLastResponseHttpCode(): ?int 192 | { 193 | return $this->lastResponseHttpCode; 194 | } 195 | 196 | public function getConnectionTime(bool $tillLastRequest = false) 197 | { 198 | return $tillLastRequest ? $this->getConnectionTimeTillLastRequest() : $this->getConnectionTimeTillNow(); 199 | } 200 | 201 | private function getConnectionTimeTillLastRequest() 202 | { 203 | if (!$this->lastResponseEndTime || !$this->connectionStartTime) { 204 | return null; 205 | } 206 | 207 | return $this->lastResponseEndTime - $this->connectionStartTime; 208 | } 209 | 210 | private function getConnectionTimeTillNow() 211 | { 212 | if (!$this->connectionStartTime) { 213 | return null; 214 | } 215 | 216 | return microtime(true) - $this->connectionStartTime; 217 | } 218 | 219 | public function __getLastRequest(): string 220 | { 221 | return $this->lastRequest; 222 | } 223 | 224 | public function __getLastResponse(): string 225 | { 226 | return (string)$this->lastResponse; 227 | } 228 | 229 | public function setTimeout($milliseconds): void 230 | { 231 | $this->timeout = $milliseconds; 232 | } 233 | 234 | public function getTimeout() 235 | { 236 | return $this->timeout; 237 | } 238 | 239 | public function setConnectTimeout($milliseconds): void 240 | { 241 | $this->connectTimeout = $milliseconds; 242 | } 243 | 244 | public function getConnectTimeout() 245 | { 246 | return $this->connectTimeout; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/Dispatcher.php: -------------------------------------------------------------------------------- 1 | checkRequirements(); 55 | $this->certificate = $certificate; 56 | 57 | if ($service === self::PLAYGROUND_SERVICE) { 58 | $this->setPlaygroundService(); 59 | } elseif ($service === self::PRODUCTION_SERVICE) { 60 | $this->setProductionService(); 61 | } else { 62 | $this->setService($service); 63 | } 64 | 65 | if ($validate) { 66 | $this->initValidator(); 67 | } 68 | } 69 | 70 | public function setService($service): void 71 | { 72 | $this->service = $service; 73 | } 74 | 75 | public function setPlaygroundService(): void 76 | { 77 | $this->setService(__DIR__ . '/Schema/PlaygroundService.wsdl'); 78 | } 79 | 80 | public function setProductionService(): void 81 | { 82 | $this->setService(__DIR__ . '/Schema/ProductionService.wsdl'); 83 | } 84 | 85 | public function getService(): string 86 | { 87 | return $this->service; 88 | } 89 | 90 | public function check(Receipt $receipt): bool 91 | { 92 | try { 93 | $this->send($receipt, true); 94 | 95 | return true; 96 | } catch (Exceptions\EET\ErrorException $e) { 97 | return false; 98 | } 99 | } 100 | 101 | public function test(Receipt $receipt, bool $hiddenSensitiveData = true): void 102 | { 103 | $this->check($receipt); 104 | 105 | $debugger = new Debugger\LastRequest($this->soapClient->lastRequest); 106 | $debugger->hiddenSensitiveData = $hiddenSensitiveData; 107 | $debugger->out(); 108 | } 109 | 110 | public function getCheckCodes(Receipt $receipt): array 111 | { 112 | if (isset($this->validator)) { 113 | $violations = $this->validator->validate($receipt); 114 | 115 | if ($violations->count() > 0) { 116 | throw new Exceptions\Receipt\ConstraintViolationException($violations); 117 | } 118 | } 119 | 120 | if (isset($receipt->bkp, $receipt->pkp)) { 121 | $this->pkp = $receipt->pkp; 122 | $this->bkp = $receipt->bkp; 123 | } else { 124 | $objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']); 125 | $objKey->loadKey($this->certificate->getPrivateKey()); 126 | 127 | $arr = [ 128 | $receipt->dic_popl, 129 | $receipt->id_provoz, 130 | $receipt->id_pokl, 131 | $receipt->porad_cis, 132 | $receipt->dat_trzby->format('c'), 133 | Format::price($receipt->celk_trzba) 134 | ]; 135 | 136 | $this->pkp = $objKey->signData(implode('|', $arr)); 137 | $this->bkp = Format::BKB(sha1($this->pkp)); 138 | } 139 | 140 | return [ 141 | 'pkp' => [ 142 | '_' => $this->pkp, 143 | 'digest' => 'SHA256', 144 | 'cipher' => 'RSA2048', 145 | 'encoding' => 'base64' 146 | ], 147 | 'bkp' => [ 148 | '_' => $this->bkp, 149 | 'digest' => 'SHA1', 150 | 'encoding' => 'base16' 151 | ] 152 | ]; 153 | } 154 | 155 | public function send(Receipt $receipt, bool $check = false): ?string 156 | { 157 | $this->initSoapClient(); 158 | 159 | try { 160 | $response = $this->processData($receipt, $check); 161 | } catch (Exceptions\SoapClient\CurlException $exception) { 162 | throw new Exceptions\EET\ClientException($receipt, $this->pkp, $this->bkp, $exception); 163 | } 164 | 165 | if (isset($response->Chyba)) { 166 | $this->processError($response->Chyba); 167 | } 168 | 169 | if (isset($response->Varovani)) { 170 | $this->processWarnings($response->Varovani); 171 | } 172 | 173 | $this->fik = $check ? null : $response->Potvrzeni->fik; 174 | 175 | return $this->fik; 176 | } 177 | 178 | public function getSoapClient(): SoapClient 179 | { 180 | if (!isset($this->soapClient)) { 181 | $this->initSoapClient(); 182 | } 183 | 184 | return $this->soapClient; 185 | } 186 | 187 | public function prepareData(Receipt $receipt, bool $check = false): array 188 | { 189 | $this->sentDateTime = new DateTime; 190 | $head = $receipt->buildHeader(); 191 | $head += [ 192 | 'dat_odesl' => $this->sentDateTime->format('c'), 193 | 'overeni' => $check 194 | ]; 195 | 196 | $this->lastReceipt = $receipt; 197 | 198 | return [ 199 | 'Hlavicka' => $head, 200 | 'Data' => $receipt->buildBody(), 201 | 'KontrolniKody' => $this->getCheckCodes($receipt) 202 | ]; 203 | } 204 | 205 | public function getBkp(): ?string 206 | { 207 | return $this->bkp; 208 | } 209 | 210 | public function getPkp(bool $encoded = true): ?string 211 | { 212 | $pkp = $this->pkp; 213 | 214 | if ($pkp === null) { 215 | return null; 216 | } 217 | 218 | if ($encoded) { 219 | $pkp = base64_encode($pkp); 220 | } 221 | 222 | return $pkp; 223 | } 224 | 225 | public function getSentDateTime(): ?DateTime 226 | { 227 | return $this->sentDateTime; 228 | } 229 | 230 | public function getFik(): ?string 231 | { 232 | return $this->fik; 233 | } 234 | 235 | public function getLastReceipt(): ?Receipt 236 | { 237 | return $this->lastReceipt; 238 | } 239 | 240 | public function getWarnings(): array 241 | { 242 | return $this->lastWarnings; 243 | } 244 | 245 | public function setCurlOption(int $option, $value = null): self 246 | { 247 | $this->curlOptions[$option] = $value; 248 | 249 | return $this; 250 | } 251 | 252 | private function checkRequirements(): void 253 | { 254 | if (!class_exists(\SoapClient::class)) { 255 | throw new Exceptions\ExtensionNotFound('php_soap.dll'); 256 | } 257 | } 258 | 259 | private function processData(Receipt $receipt, bool $check = false) 260 | { 261 | $data = $this->prepareData($receipt, $check); 262 | 263 | return $this->getSoapClient()->OdeslaniTrzby($data); 264 | } 265 | 266 | private function processError($error): void 267 | { 268 | if ($error->kod) { 269 | $msg = Enum\Error::LIST[$error->kod] ?? ''; 270 | 271 | throw new Exceptions\EET\ErrorException($msg, $error->kod); 272 | } 273 | } 274 | 275 | private function processWarnings($warnings): void 276 | { 277 | $this->lastWarnings = []; 278 | 279 | if (is_array($warnings)) { 280 | foreach ($warnings as $warning) { 281 | $this->lastWarnings[] = [ 282 | 'code' => $warning->kod_varov, 283 | 'message' => Enum\Warning::LIST[$warning->kod_varov] ?? '' 284 | ]; 285 | } 286 | } else { 287 | $this->lastWarnings[] = [ 288 | 'code' => $warnings->kod_varov, 289 | 'message' => Enum\Warning::LIST[$warnings->kod_varov] ?? '' 290 | ]; 291 | } 292 | } 293 | 294 | private function initSoapClient(): void 295 | { 296 | if (!isset($this->service)) { 297 | throw new Exceptions\RuntimeException('Service is not set. Use self::set(Production|Playground)Service()'); 298 | } 299 | 300 | if (!isset($this->soapClient)) { 301 | $this->soapClient = new SoapClient($this->service, $this->certificate, false, $this->curlOptions); 302 | } 303 | } 304 | 305 | private function initValidator(): void 306 | { 307 | if (!isset($this->validator)) { 308 | $this->validator = $this->buildValidatorInterface(); 309 | } 310 | } 311 | 312 | private function buildValidatorInterface(): ValidatorInterface 313 | { 314 | return Validation::createValidatorBuilder() 315 | ->addMethodMapping('loadValidatorMetadata') 316 | ->getValidator(); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/Schema/EETXMLSchema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | --------------------------------------------------------------------------------