├── 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 |
--------------------------------------------------------------------------------