diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef551f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +nbproject/* +vendor/ \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..434b1a1 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 Miloš Havlíček + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md index 3eea3ce..bf878cd 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,52 @@ -# Ukázka implementace EET v PHP. +# Example implementation of EET in PHP + +## Installation +Install Ondrejnov/eet using [Composer](http://getcomposer.org/): + +```sh +$ composer require cothema/eet +``` + +### Dependencies +PHP >=5.6 +robrichards/wse-php + +Attached WSDL, key and certificate are intended for non-production usage (Playground). + +## Example Usage +Sample codes are located in examples/ folder + +### License +MIT + +--- + +# Ukázka implementace EET v PHP + +## Instalace +Install Ondrejnov/eet using [Composer](http://getcomposer.org/): + +```sh +$ composer require cothema/eet +``` ### Závislosti -Testováno na PHP 5.6 +PHP >=5.6 +robrichards/wse-php -V adresáří vendor se očekává knihovna https://github.com/robrichards/wse-php +Přiložené WSDL, klíč a certifikát jsou pro neprodukční prostředí (Playground). -Přiložené WSDL, klíč a certifikát je pro neprodukční prostředí (Playground). +## Ukázka použití +Ukázky použití naleznete ve složce examples/ ### Licence -Kód je poskytován tak jak stojí a leží, můžete s ním dělat co chcete, klidně použít i pro EET. Za chyby nezodpovídám. +MIT + +--- ### Reklama Komu se nechce do implementace, tak může použít on-line službu EETApp.cz, která má pokročilejší správu účtenek včetně tisku na tiskárnu. - + ### Bitcoin Donate 1LZuWFUHeVMrYvZWinxFjjkZtuq56TECot diff --git a/eet.key b/_cert/eet.key similarity index 100% rename from eet.key rename to _cert/eet.key diff --git a/eet.pem b/_cert/eet.pem similarity index 100% rename from eet.pem rename to _cert/eet.pem diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ac98d5a --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "ondrejnov/eet", + "description": "EET (Electronic records of sales for Czech Ministry of Finance) Client for PHP", + "type": "project", + "license": ["MIT", "BSD-3-Clause", "GPL-2.0", "GPL-3.0"], + "require": { + "php": ">=5.6.0", + "robrichards/wse-php": "*" + }, + "require-dev": { + "nette/tester": "~1.4" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "classmap": ["src/"] + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..122f0fd --- /dev/null +++ b/composer.lock @@ -0,0 +1,108 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "c658f2c59e9ea90ac5e176dac443cd80", + "content-hash": "db9c721cc0d3a114db5a58c6a7378e8f", + "packages": [ + { + "name": "robrichards/wse-php", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/robrichards/wse-php.git", + "reference": "ed6c3cefaa80604de539e69b0affaca67e11195e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/robrichards/wse-php/zipball/ed6c3cefaa80604de539e69b0affaca67e11195e", + "reference": "ed6c3cefaa80604de539e69b0affaca67e11195e", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "": "src/" + }, + "files": [ + "xmlseclibs.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD" + ], + "description": "Libraries for adding WS-* support to ext/soap in PHP.", + "time": "2016-01-29 10:34:55" + } + ], + "packages-dev": [ + { + "name": "nette/tester", + "version": "v1.7.1", + "source": { + "type": "git", + "url": "https://github.com/nette/tester.git", + "reference": "d97534578e8cf66eabe081e7d5eaa4dd527ab0c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/tester/zipball/d97534578e8cf66eabe081e7d5eaa4dd527ab0c8", + "reference": "d97534578e8cf66eabe081e7d5eaa4dd527ab0c8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "bin": [ + "src/tester" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "An easy-to-use PHP unit testing framework.", + "homepage": "https://tester.nette.org", + "keywords": [ + "nette", + "testing", + "unit" + ], + "time": "2016-03-19 14:34:02" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=5.6.0" + }, + "platform-dev": [] +} diff --git a/example.php b/example.php deleted file mode 100644 index f3c64c6..0000000 --- a/example.php +++ /dev/null @@ -1,26 +0,0 @@ -uuid_zpravy = 'b3a09b52-7c87-4014-a496-4c7a53cf9120'; -$r->dic_popl = 'CZ72080043'; -$r->id_provoz = '181'; -$r->id_pokl = '1'; -$r->porad_cis = '1'; -$r->dat_trzby = new DateTime(); -$r->celk_trzba = 1000; -$fik = $r->send(); -var_dump($fik); - -// ukazka chyby -$r->dic_popl = 'x'; -try { - $fik = $r->send(); -} -catch (\eet\EETException $e) { - var_dump($e->getMessage()); -} \ No newline at end of file diff --git a/examples/bootstrap.php b/examples/bootstrap.php new file mode 100644 index 0000000..eadadf5 --- /dev/null +++ b/examples/bootstrap.php @@ -0,0 +1,10 @@ +uuid_zpravy = 'b3a09b52-7c87-4014-a496-4c7a53cf9120'; +$r->dic_popl = 'CZ72080043'; +$r->id_provoz = '181'; +$r->id_pokl = '1'; +$r->porad_cis = '1'; +$r->dat_trzby = new \DateTime(); +$r->celk_trzba = 1000; + +// Valid response should be returned +echo '

---VALID REQUEST---

'; +try { + $fik = $dispatcher->send($r); // Send request + echo sprintf('Returned fik code: %s', $fik); // See response - should be returned +} catch (ServerException $e) { + var_dump($e); // See exception +} catch (\Exception $e) { + var_dump($e); // Fatal error +} + +// Example of error message +$r->dic_popl = 'x'; + +// ServerException should be returned +echo '

---ERROR REQUEST---

'; +try { + $fik = $dispatcher->send($r); // Send request + var_dump($fik); // See response +} catch (ServerException $e) { + echo sprintf('Error from server of Ministry of Finance: %s', $e->getMessage()); // See exception - should be returned +} catch (\Exception $e) { + var_dump($e); // Fatal error +} \ No newline at end of file diff --git a/lib/EETException.php b/lib/EETException.php deleted file mode 100644 index ba72c59..0000000 --- a/lib/EETException.php +++ /dev/null @@ -1,7 +0,0 @@ -key = $key; - $this->cert = $cert; - } - - public function check() - { - try { - return $this->send(TRUE); - } - catch (EETException $e) { - return FALSE; - } - } - - public function send($check = FALSE) - { - $hlavicka = [ - 'uuid_zpravy' => $this->uuid_zpravy, - 'dat_odesl' => time(), - 'prvni_zaslani' => $this->prvni_zaslani, - 'overeni' => $check - ]; - - $data = [ - 'dic_popl' => $this->dic_popl, - 'dic_poverujiciho' => $this->dic_poverujiciho, - 'id_provoz' => $this->id_provoz, - 'id_pokl' => $this->id_pokl, - 'porad_cis' => $this->porad_cis, - 'dat_trzby' => $this->dat_trzby->format('c'), - 'celk_trzba' => $this->priceFormat($this->celk_trzba), - 'zakl_nepodl_dph' => $this->priceFormat($this->zakl_nepodl_dph), - 'zakl_dan1' => $this->priceFormat($this->zakl_dan1), - 'dan1' => $this->priceFormat($this->dan1), - 'zakl_dan2' => $this->priceFormat($this->zakl_dan2), - 'dan2' => $this->priceFormat($this->dan2), - 'zakl_dan3' => $this->priceFormat($this->zakl_dan3), - 'dan3' => $this->priceFormat($this->dan3), - 'rezim' => $this->rezim - ]; - - - $soapClient = new \eet\SoapClient($this->key, $this->cert); - $response = $soapClient->OdeslaniTrzby([ - 'Hlavicka' => $hlavicka, - 'Data' => $data, - 'KontrolniKody' => $this->getCheckCodes() - ] - ); - if (isset($response->Chyba)) { - $this->processError($response->Chyba); - } - if ($check) { - return TRUE; - } - else { - return $response->Potvrzeni->fik; - } - } - - public function getCheckCodes() - { - $objKey = new \XMLSecurityKey(\XMLSecurityKey::RSA_SHA256, ['type' => 'private']); - $objKey->loadKey($this->key, TRUE); - - $arr = [ - $this->dic_popl, - $this->id_provoz, - $this->id_pokl, - $this->porad_cis, - $this->dat_trzby->format('c'), - $this->priceFormat($this->celk_trzba) - ]; - $sign = $objKey->signData(join('|', $arr)); - - return [ - 'pkp' => [ - '_' => $sign, - 'digest' => 'SHA256', - 'cipher' => 'RSA2048', - 'encoding' => 'base64' - ], - 'bkp' => [ - '_' => $this->formatBKB(sha1($sign)), - 'digest' => 'SHA1', - 'encoding' => 'base16' - ] - ]; - } - - private function formatBKB($code) { - $r = ''; - for ($i = 0; $i < 40; $i++) { - if ($i % 8 == 0 && $i != 0) { - $r.= '-'; - } - $r .= $code[$i]; - } - return $r; - } - - private function priceFormat($value) { - return number_format($value, 2, '.', ''); - } - - private function processError($error) { - if ($error->kod) { - $msgs = [ - -1 => 'Docasna technicka chyba zpracovani – odeslete prosim datovou zpravu pozdeji', - 2 => 'Kodovani XML neni platne', - 3 => 'XML zprava nevyhovela kontrole XML schematu', - 4 => 'Neplatny podpis SOAP zpravy', - 5 => 'Neplatny kontrolni bezpecnostni kod poplatnika (BKP)', - 6 => 'DIC poplatnika ma chybnou strukturu', - 7 => 'Datova zprava je prilis velka', - 8 => 'Datova zprava nebyla zpracovana kvuli technicke chybe nebo chybe dat', - ]; - $msg = isset($msgs[$error->kod]) ? $msgs[$error->kod] : ''; - throw new EETException($msg, $error->kod); - } - } - -} \ No newline at end of file diff --git a/lib/SoapClient.php b/lib/SoapClient.php deleted file mode 100644 index d9b3df7..0000000 --- a/lib/SoapClient.php +++ /dev/null @@ -1,33 +0,0 @@ - 1]); - $this->key = $key; - $this->cert = $cert; - } - - public function __doRequest($request, $location, $saction, $version, $one_way = NULL) - { - $doc = new \DOMDocument('1.0'); - $doc->loadXML($request); - - $objWSSE = new \WSSESoap($doc); - $objWSSE->addTimestamp(); - - $objKey = new \XMLSecurityKey(\XMLSecurityKey::RSA_SHA256, ['type' => 'private']); - $objKey->loadKey($this->key, TRUE); - $objWSSE->signSoapDoc($objKey, ["algorithm" => \XMLSecurityDSig::SHA256]); - - $token = $objWSSE->addBinaryToken(file_get_contents($this->cert)); - $objWSSE->attachTokentoSig($token); - - return parent::__doRequest($objWSSE->saveXML(), $location, $saction, $version); - } -} \ No newline at end of file diff --git a/src/Dispatcher.php b/src/Dispatcher.php new file mode 100644 index 0000000..f5c98be --- /dev/null +++ b/src/Dispatcher.php @@ -0,0 +1,162 @@ +key = $key; + $this->cert = $cert; + $this->checkRequirements(); + } + + private function checkRequirements() { + if (!class_exists('\SoapClient')) { + throw new RequirementsException('Class SoapClient is not defined! Please, allow php extension php_soap.dll in php.ini'); + } + } + + /** + * + * @param Receipt $receipt + * @return boolean|string + */ + public function check(Receipt $receipt) { + try { + return $this->send($receipt, TRUE); + } catch (ServerException $e) { + return FALSE; + } + } + + /** + * + * @param Receipt $receipt + * @param boolean $check + * @return boolean|string + */ + public function send(Receipt $receipt, $check = FALSE) { + $response = $this->processData($receipt, $check); + + isset($response->Chyba) && $this->processError($response->Chyba); + + return $check ? TRUE : $response->Potvrzeni->fik; + } + + /** + * + * @param Receipt $receipt + * @param boolean $check + * @return object + */ + private function processData(Receipt $receipt, $check = FALSE) { + $head = [ + 'uuid_zpravy' => $receipt->uuid_zpravy, + 'dat_odesl' => time(), + 'prvni_zaslani' => $receipt->prvni_zaslani, + 'overeni' => $check + ]; + + $body = [ + 'dic_popl' => $receipt->dic_popl, + 'dic_poverujiciho' => $receipt->dic_poverujiciho, + 'id_provoz' => $receipt->id_provoz, + 'id_pokl' => $receipt->id_pokl, + 'porad_cis' => $receipt->porad_cis, + 'dat_trzby' => $receipt->dat_trzby->format('c'), + 'celk_trzba' => Format::price($receipt->celk_trzba), + 'zakl_nepodl_dph' => Format::price($receipt->zakl_nepodl_dph), + 'zakl_dan1' => Format::price($receipt->zakl_dan1), + 'dan1' => Format::price($receipt->dan1), + 'zakl_dan2' => Format::price($receipt->zakl_dan2), + 'dan2' => Format::price($receipt->dan2), + 'zakl_dan3' => Format::price($receipt->zakl_dan3), + 'dan3' => Format::price($receipt->dan3), + 'rezim' => $receipt->rezim + ]; + + + $soapClient = new SoapClient($this->key, $this->cert); + return $soapClient->OdeslaniTrzby([ + 'Hlavicka' => $head, + 'Data' => $body, + 'KontrolniKody' => $this->getCheckCodes($receipt) + ] + ); + } + + public function getCheckCodes(Receipt $receipt) { + $objKey = new \XMLSecurityKey(\XMLSecurityKey::RSA_SHA256, ['type' => 'private']); + $objKey->loadKey($this->key, TRUE); + + $arr = [ + $receipt->dic_popl, + $receipt->id_provoz, + $receipt->id_pokl, + $receipt->porad_cis, + $receipt->dat_trzby->format('c'), + Format::price($receipt->celk_trzba) + ]; + $sign = $objKey->signData(join('|', $arr)); + + return [ + 'pkp' => [ + '_' => $sign, + 'digest' => 'SHA256', + 'cipher' => 'RSA2048', + 'encoding' => 'base64' + ], + 'bkp' => [ + '_' => Format::BKB(sha1($sign)), + 'digest' => 'SHA1', + 'encoding' => 'base16' + ] + ]; + } + + private function processError($error) { + if ($error->kod) { + $msgs = [ + -1 => 'Docasna technicka chyba zpracovani – odeslete prosim datovou zpravu pozdeji', + 2 => 'Kodovani XML neni platne', + 3 => 'XML zprava nevyhovela kontrole XML schematu', + 4 => 'Neplatny podpis SOAP zpravy', + 5 => 'Neplatny kontrolni bezpecnostni kod poplatnika (BKP)', + 6 => 'DIC poplatnika ma chybnou strukturu', + 7 => 'Datova zprava je prilis velka', + 8 => 'Datova zprava nebyla zpracovana kvuli technicke chybe nebo chybe dat', + ]; + $msg = isset($msgs[$error->kod]) ? $msgs[$error->kod] : ''; + throw new ServerException($msg, $error->kod); + } + } + +} diff --git a/src/Exceptions/RequirementsException.php b/src/Exceptions/RequirementsException.php new file mode 100644 index 0000000..a72028a --- /dev/null +++ b/src/Exceptions/RequirementsException.php @@ -0,0 +1,7 @@ + - - - Ucel : Sluzba pro odeslani datove zpravy evidovane trzby - Verze : 2.0 - Vlastnik : Generalni financni reditelstvi - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + Ucel : Sluzba pro odeslani datove zpravy evidovane trzby + Verze : 2.0 + Vlastnik : Generalni financni reditelstvi + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EETXMLSchema.xsd b/src/Schema/EETXMLSchema.xsd similarity index 97% rename from EETXMLSchema.xsd rename to src/Schema/EETXMLSchema.xsd index 53b4390..abbdf7b 100644 --- a/EETXMLSchema.xsd +++ b/src/Schema/EETXMLSchema.xsd @@ -1,200 +1,200 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SoapClient.php b/src/SoapClient.php new file mode 100644 index 0000000..327dee6 --- /dev/null +++ b/src/SoapClient.php @@ -0,0 +1,33 @@ + 1]); + $this->key = $key; + $this->cert = $cert; + } + + public function __doRequest($request, $location, $saction, $version, $one_way = NULL) { + $doc = new \DOMDocument('1.0'); + $doc->loadXML($request); + + $objWSSE = new \WSSESoap($doc); + $objWSSE->addTimestamp(); + + $objKey = new \XMLSecurityKey(\XMLSecurityKey::RSA_SHA256, ['type' => 'private']); + $objKey->loadKey($this->key, TRUE); + $objWSSE->signSoapDoc($objKey, ["algorithm" => \XMLSecurityDSig::SHA256]); + + $token = $objWSSE->addBinaryToken(file_get_contents($this->cert)); + $objWSSE->attachTokentoSig($token); + + return parent::__doRequest($objWSSE->saveXML(), $location, $saction, $version); + } + +} diff --git a/src/Utils/Format.php b/src/Utils/Format.php new file mode 100644 index 0000000..0da54b7 --- /dev/null +++ b/src/Utils/Format.php @@ -0,0 +1,22 @@ +getTestDispatcher()->send($this->getExampleReceipt()); + Assert::type('string', $fik); + } + + public function testSendError() { + $r = $this->getExampleReceipt(); + $r->dic_popl = 'x'; + Assert::exception(function() use ($r) { + $this->getTestDispatcher()->send($r); + }, ServerException::class); + } + + /** + * + * @return Tested + */ + private function getTestDispatcher() { + return new Tested(DIR_CERT . '/eet.key', DIR_CERT . '/eet.pem'); + } + + /** + * @return Receipt + */ + private function getExampleReceipt() { + $r = new Receipt(); + $r->uuid_zpravy = 'b3a09b52-7c87-4014-a496-4c7a53cf9120'; + $r->dic_popl = 'CZ72080043'; + $r->id_provoz = '181'; + $r->id_pokl = '1'; + $r->porad_cis = '1'; + $r->dat_trzby = new \DateTime(); + $r->celk_trzba = 1000; + return $r; + } + +} + +(new Dispatcher)->run(); diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..eadadf5 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,10 @@ +