diff --git a/.gitignore b/.gitignore
index 4a2b948..72ab052 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,16 @@
.DS_Store
-composer.lock
.idea/
vendor/
develop/
coverage/
test.php
+/nbproject/private/
+/nbproject/
+/composer.lock
+/composer.phar
+/Samples/config.php
+/Samples/tan.txt
+/Samples/*.test.php
+/Samples/*.log
+/Samples/analyzeLogs.php
+/doc/
diff --git a/.travis.yml b/.travis.yml
index 2573431..611ba05 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,36 +1,12 @@
language: php
-
-## Run on container environment
-sudo: false
-
-## Cache composer bits
-cache:
- directories:
- - $HOME/.composer/cache
-
-## List all PHP versions to test with
+install: composer install
+script:
+ - ./phplint.sh ./lib/
+ - phpunit
php:
- - 5.4
- - 5.5
- - 5.6
- - 7.0
- - 7.1
+ - '5.4'
+ - '5.5'
+ - '5.6'
+ - '7.0'
- hhvm
- nightly
-
-matrix:
- fast_finish: true
- allow_failures:
- - php: nightly
- - php: hhvm
-
-## Install Dependencies
-install:
- - composer self-update
- - if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi;
- - composer install --prefer-dist --no-interaction
-
-## Run test Scripts
-script:
- - ./phplint.sh ./lib/
- - vendor/bin/phpunit
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
deleted file mode 100644
index 87d4bc7..0000000
--- a/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
-
-## Our Standards
-
-Examples of behavior that contributes to creating a positive environment include:
-
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
-
-Examples of unacceptable behavior by participants include:
-
-* The use of sexualized language or imagery and unwelcome sexual attention or advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or electronic address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a professional setting
-
-## Our Responsibilities
-
-Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
-
-Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
-
-## Scope
-
-This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at markus.schindler1@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
-
-Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
-
-[homepage]: http://contributor-covenant.org
-[version]: http://contributor-covenant.org/version/1/4/
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index 7eb1d46..1f7e3d5 100755
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -45,6 +45,7 @@ This library is verified to work with following banks.
- [x] Bankhaus Rautenschlein
- [ ] Bankverein Bebra
- [ ] Bankverein Werther AG
+- [ ] Barclays Bank PLC
- [ ] Bayerische Landesbank Girozentrale
- [x] BBBank
- [ ] Bensberger Bank eG
@@ -72,6 +73,7 @@ This library is verified to work with following banks.
- [ ] Cash Express Gesellschaft f Finanz-u Reisedienstleistungen
- [x] comdirect bank AG
- [ ] Commerzbank
+- [x] Consorsbank
- [ ] Crailsheimer Volksbank -alt-
- [ ] Credit- und Volksbank eG
- [x] Cronbank
@@ -79,7 +81,7 @@ This library is verified to work with following banks.
- [ ] Darmsheimer Bank
- [ ] Degussa Bank AG
- [ ] Dettinger Bank
-- [ ] Deutsche Apotheker- und Ärztebank eG
+- [x] Deutsche Apotheker- und Ärztebank eG
- [ ] Deutsche Bank AG
- [x] Deutsche Bank Privat und Geschäftskunden AG
- [ ] Deutsche Genossenschafts-Hypothekenbank AG
@@ -277,7 +279,7 @@ This library is verified to work with following banks.
- [ ] Münsterländische Bank Thie & Co
- [ ] Murgtalbank Mitteltal - Obertal -alt-
- [ ] Müritz-Sparkasse
-- [ ] Nassauische Sparkasse
+- [X] Nassauische Sparkasse
- [ ] National-Bank
- [ ] net-m privatbank 1891 AG
- [ ] netbank AG
@@ -285,7 +287,7 @@ This library is verified to work with following banks.
- [ ] NIBC Bank Zndl Frankfurt am Main
- [ ] NL Bank Volks- und Raiffeisenbank eG
- [ ] Nord-Ostsee Sparkasse
-- [ ] Nord/LB Hannover
+- [X] Nord/LB Hannover / Norddeutsche Landesbank Girozentrale
- [ ] Norderstedter Bank eG
- [ ] Nordthüringer Volksbank
- [ ] norisbank GmbH
@@ -532,6 +534,7 @@ This library is verified to work with following banks.
- [ ] Raiffeisenbank Leezen eG
- [ ] Raiffeisenbank Lorup eG
- [x] Raiffeisenbank Maintal Ndl d Frankfurter VB
+- [x] Raiffeisenbank Main-Spessart
- [ ] Raiffeisenbank Maitis
- [ ] Raiffeisenbank Malchin eG
- [ ] Raiffeisenbank Mangfalltal -alt-
@@ -874,7 +877,7 @@ This library is verified to work with following banks.
- [ ] Sparkasse Kierspe-Meinerzhagen
- [ ] Sparkasse Kleve
- [ ] Sparkasse Koblenz
-- [ ] Sparkasse KölnBonn
+- [x] Sparkasse KölnBonn
- [ ] Sparkasse Kraichgau (Bruchsal-Bretten-Sinsheim)
- [x] Sparkasse Krefeld
- [ ] Sparkasse Kulmbach-Kronach
@@ -909,7 +912,7 @@ This library is verified to work with following banks.
- [ ] Sparkasse Mittelholstein AG
- [ ] Sparkasse Mittelmosel-Eifel-Mosel-Hunsrück
- [ ] Sparkasse Mittelsachsen
-- [ ] Sparkasse Mittelthüringen
+- [x] Sparkasse Mittelthüringen
- [ ] Sparkasse Moosburg
- [ ] Sparkasse Muldental
- [ ] Sparkasse Mülheim a.d. Ruhr
@@ -1013,7 +1016,7 @@ This library is verified to work with following banks.
- [ ] Stadt- und Kreissparkasse Rothenburg
- [ ] Stadt-Sparkasse Düsseldorf
- [ ] Stadt-Sparkasse Haan (Rheinland)
-- [ ] Stadt-Sparkasse Langenfeld
+- [x] Stadt-Sparkasse Langenfeld
- [ ] Stadt-Sparkasse Solingen
- [ ] Stadtsparkasse Aichach-Schrobenhausen
- [x] Stadtsparkasse Augsburg
@@ -1071,7 +1074,7 @@ This library is verified to work with following banks.
- [ ] Stuttgarter Volksbank
- [ ] Südwestbank
- [ ] Sydbank A/S
-- [ ] Taunus-Sparkasse
+- [x] Taunus-Sparkasse
- [x] Triodos Bank Deutschland
- [ ] Uhlbacher Bank
- [ ] UniCredit Bank - HypoVereinsbank AG
@@ -1097,7 +1100,7 @@ This library is verified to work with following banks.
- [ ] VB Nahetal eG
- [ ] VB Nordmünsterland Mitte eG
- [ ] VB RB eG, Neumünster
-- [ ] VB RheinAhrEifel eG.
+- [x] VB RheinAhrEifel eG.
- [ ] VB Spelle-Freren eG
- [ ] VB Südkirchen-Cap.-Nordkirchen
- [ ] VB Vallendar-Niederwerth eG
diff --git a/README.md b/README.md
index 467c328..68c7872 100644
--- a/README.md
+++ b/README.md
@@ -1,44 +1,41 @@
-# FinTS HBCI PHP
+# PHP FinTS/HBCI library
-[](https://travis-ci.org/mschindler83/fints-hbci-php)
-[](https://packagist.org/packages/mschindler83/fints-hbci-php)
-[](https://scrutinizer-ci.com/g/mschindler83/fints-hbci-php/?branch=master)
-[](https://packagist.org/packages/mschindler83/fints-hbci-php)
-[](https://github.com/mschindler83/fints-hbci-php/blob/master/LICENSE)
+A PHP library implementing the following functions of the FinTS/HBCI protocol:
-A PHP library implementing the basics of the FinTS / HBCI protocol.
-It can be used to fetch the balance of connected bank accounts and for fetching bank statements of accounts.
+ * Get accounts
+ * Get bank parameters
+ * Get balance
+ * Get transactions
+ * Get standing orders
+ * Delete standing order
+ * Execute direct debit
+ * Execute transfer
+
+Forked from https://github.com/mschindler83/fints-hbci-php
## Getting Started
Install via composer:
-```bash
-composer require mschindler83/fints-hbci-php
-```
-
-## How to use it
+ composer require nemiah/php-fints
-You can have a look at the "Samples" folder in this repository.
-Just fill in the required data beginning from line 13 to 17 and run the script.
-You can find the server information of your bank here:
-https://www.hbci-zka.de/institute/institut_auswahl.htm
+## Usage
-## Contribute
+See the examples in the "Samples" folder.
+Fill out the required configuration and execute the file.
-### Bank compatibility
+Server details can be obtained here after registration:
+https://www.hbci-zka.de
-This library can only work stable with *YOUR* help!
-As I'm very limited in testing different banks it would be good to get some feedback from you all.
-Feel free to create PR's for the [COMPATIBILITY.md](COMPATIBILITY.md) file where you can update the list of working banks.
+## Special usage
-### Code Style
+My goal for this library was to be able to execute automatic direct debits with m(obile)TAN.
+
+## Contribute
-If you plan to contribute to this library, please ensure that you stick with the PSR coding rules as close as you can (At least PSR-0 to PSR-4).
-You can find the PHP Standard Recommendations [here](http://www.php-fig.org/psr/)
+Contributions are welcome!
-### Have fun!
+### Bank compatibility
-Looking for a PRO Version which is capable of doing transactions?
-Checkout http://fints-hbci-php.markus-schindler.de/pro.html
+Please update the COMPATIBILITY.md file and send a pull request if you successfully tested this library with your bank.
diff --git a/Samples/accounts.php b/Samples/accounts.php
new file mode 100644
index 0000000..17014d4
--- /dev/null
+++ b/Samples/accounts.php
@@ -0,0 +1,43 @@
+getSEPAAccounts();
+
+$fints->end();
+print_r($accounts);
diff --git a/Samples/directDebit.php b/Samples/directDebit.php
new file mode 100644
index 0000000..e4b2041
--- /dev/null
+++ b/Samples/directDebit.php
@@ -0,0 +1,86 @@
+add(new \DateInterval("P1D"));
+
+$sepaDD = new SEPADirectDebitBasic(array(
+ 'messageID' => time(),
+ 'paymentID' => time(),
+ 'type' => "COR1"
+));
+
+$sepaDD->setCreditor(new SEPACreditor(array( //this is you
+ 'name' => 'My Company',
+ 'iban' => 'DE68210501700012345678',
+ 'bic' => 'DEUTDEDB400',
+ 'identifier' => 'DE98ZZZ09999999999'
+)));
+
+$sepaDD->addDebitor(new SEPADebitor(array( //this is who you want to get money from
+ 'transferID' => "R20170100",
+ 'mandateID' => "aeicznaeibcnt",
+ 'mandateDateOfSignature' => "2017-05-05",
+ 'name' => 'Max Mustermann',
+ 'iban' => 'CH9300762011623852957',
+ 'bic' => 'GENODEF1P15',
+ 'amount' => 48.78,
+ 'currency' => 'EUR',
+ 'info' => "R20170100 vom 09.05.2017",
+ 'requestedCollectionDate' => $dt,
+ 'sequenceType' => "OOFF"
+)));
+
+
+
+use Fhp\FinTs;
+
+define('FHP_BANK_URL', ''); # HBCI / FinTS Url can be found here: https://www.hbci-zka.de/institute/institut_auswahl.htm (use the PIN/TAN URL)
+define('FHP_BANK_PORT', 443); # HBCI / FinTS Port can be found here: https://www.hbci-zka.de/institute/institut_auswahl.htm
+define('FHP_BANK_CODE', ''); # Your bank code / Bankleitzahl
+define('FHP_ONLINE_BANKING_USERNAME', ''); # Your online banking username / alias
+define('FHP_ONLINE_BANKING_PIN', ''); # Your online banking PIN (NOT! the pin of your bank card!)
+define('FHP_REGISTRATION_NO', ''); # The number you receive after registration / FinTS-Registrierungsnummer
+define('FHP_SOFTWARE_VERSION', '1.0'); # Your own Software product version
+
+$fints = new FinTs(
+ FHP_BANK_URL,
+ FHP_BANK_PORT,
+ FHP_BANK_CODE,
+ FHP_ONLINE_BANKING_USERNAME,
+ FHP_ONLINE_BANKING_PIN,
+ new testLogger(),
+ FHP_REGISTRATION_NO,
+ FHP_SOFTWARE_VERSION
+);
+
+$fints->setTANMechanism(901); //901 for mobileTAN
+file_put_contents(__DIR__."/tan.txt", "");
+
+$accounts = $fints->getSEPAAccounts();
+
+$transfer = $fints->executeSEPADirectDebit($accounts[0], $sepaDD->toXML(), function(){
+ return file_get_contents(__DIR__."/tan.txt");
+});
+
+$fints->end();
+print_r($transfer);
\ No newline at end of file
diff --git a/Samples/saldo.php b/Samples/saldo.php
index ccc0d6d..9771bc8 100644
--- a/Samples/saldo.php
+++ b/Samples/saldo.php
@@ -6,6 +6,16 @@
require '../vendor/autoload.php';
+class testLogger extends Psr\Log\AbstractLogger {
+
+ public function log($level, $message, array $context = array()): void {
+ file_put_contents(__DIR__."/state.log", file_get_contents(__DIR__."/state.log").str_replace("'", "'\n", $message)."\n");
+ }
+
+}
+
+file_put_contents(__DIR__."/saldo.log", "");
+
use Fhp\FinTs;
use Fhp\Model\StatementOfAccount\Statement;
use Fhp\Model\StatementOfAccount\Transaction;
@@ -15,18 +25,24 @@
define('FHP_BANK_CODE', ''); # Your bank code / Bankleitzahl
define('FHP_ONLINE_BANKING_USERNAME', ''); # Your online banking username / alias
define('FHP_ONLINE_BANKING_PIN', ''); # Your online banking PIN (NOT! the pin of your bank card!)
+define('FHP_REGISTRATION_NO', ''); # The number you receive after registration / FinTS-Registrierungsnummer
+define('FHP_SOFTWARE_VERSION', '1.0'); # Your own Software product version
$fints = new FinTs(
FHP_BANK_URL,
FHP_BANK_PORT,
FHP_BANK_CODE,
FHP_ONLINE_BANKING_USERNAME,
- FHP_ONLINE_BANKING_PIN
+ FHP_ONLINE_BANKING_PIN,
+ new testLogger(),
+ FHP_REGISTRATION_NO,
+ FHP_SOFTWARE_VERSION
);
$accounts = $fints->getSEPAAccounts();
$oneAccount = $accounts[0];
$saldo = $fints->getSaldo($oneAccount);
-print_r($saldo);
+$fints->end();
+print_r($saldo);
\ No newline at end of file
diff --git a/Samples/standingOrderDelete.php b/Samples/standingOrderDelete.php
new file mode 100644
index 0000000..acb1236
--- /dev/null
+++ b/Samples/standingOrderDelete.php
@@ -0,0 +1,43 @@
+getSEPAAccounts();
+file_put_contents(__DIR__."/tan.txt", "");
+
+$orders = $fints->getSEPAStandingOrders($accounts[0]);
+var_dump($orders);
+
+$fints->deleteSEPAStandingOrder($accounts[0], $orders[0], function(){
+ return file_get_contents(__DIR__."/tan.txt");
+});
\ No newline at end of file
diff --git a/Samples/standingOrders.php b/Samples/standingOrders.php
new file mode 100644
index 0000000..4028834
--- /dev/null
+++ b/Samples/standingOrders.php
@@ -0,0 +1,44 @@
+getSEPAAccounts();
+
+$orders = $fints->getSEPAStandingOrders($accounts[0]);
+
+$fints->end();
+print_r($orders);
\ No newline at end of file
diff --git a/Samples/statement_of_account.php b/Samples/statement_of_account.php
index 8180f87..e08912f 100644
--- a/Samples/statement_of_account.php
+++ b/Samples/statement_of_account.php
@@ -7,30 +7,88 @@
require '../vendor/autoload.php';
+class testLogger extends Psr\Log\AbstractLogger {
+
+ public function log($level, $message, array $context = array()): void {
+ file_put_contents(__DIR__."/accounts.log", file_get_contents(__DIR__."/accounts.log").$message."\n");
+ }
+
+}
+
use Fhp\FinTs;
use Fhp\Model\StatementOfAccount\Statement;
use Fhp\Model\StatementOfAccount\Transaction;
-define('FHP_BANK_URL', ''); # HBCI / FinTS Url can be found here: https://www.hbci-zka.de/institute/institut_auswahl.htm (use the PIN/TAN URL)
-define('FHP_BANK_PORT', 443); # HBCI / FinTS Port can be found here: https://www.hbci-zka.de/institute/institut_auswahl.htm
+// Register your application prior to begin at https://www.hbci-zka.de/register/prod_register.htm
+
+define('FHP_BANK_URL', ''); # HBCI / FinTS Url will be provided to you after registration (use the PIN/TAN URL)
+define('FHP_BANK_PORT', 443); # static standard TCP port for HTTPS
define('FHP_BANK_CODE', ''); # Your bank code / Bankleitzahl
define('FHP_ONLINE_BANKING_USERNAME', ''); # Your online banking username / alias
define('FHP_ONLINE_BANKING_PIN', ''); # Your online banking PIN (NOT! the pin of your bank card!)
+define('FHP_REGISTRATION_NO', ''); # The number you receive after registration / FinTS-Registrierungsnummer
+define('FHP_SOFTWARE_VERSION', '1.0'); # Your own Software product version
$fints = new FinTs(
FHP_BANK_URL,
FHP_BANK_PORT,
FHP_BANK_CODE,
FHP_ONLINE_BANKING_USERNAME,
- FHP_ONLINE_BANKING_PIN
+ FHP_ONLINE_BANKING_PIN,
+ new testLogger(),
+ FHP_REGISTRATION_NO,
+ FHP_SOFTWARE_VERSION
);
-$accounts = $fints->getSEPAAccounts();
+try {
+
+ $fints->setTANMechanism(901); //request available TAN modes with $fints->getVariables();!
+
+ $accounts = $fints->getSEPAAccounts();
+
+ $oneAccount = $accounts[0];
+ $from = new \DateTime('2016-01-01');
+ $to = new \DateTime();
+ $soa = $fints->getStatementOfAccount($oneAccount, $from, $to);
+
+ if($soa->isTANRequest()){
+ $serialized = serialize($soa);
+
+ echo "Waiting max. 60 seconds for TAN\n";
+
+ for($i = 0; $i < 60; $i++){
+ sleep(1);
-$oneAccount = $accounts[0];
-$from = new \DateTime('2016-01-01');
-$to = new \DateTime();
-$soa = $fints->getStatementOfAccount($oneAccount, $from, $to);
+ $tan = trim(file_get_contents(__DIR__."/tan.txt"));
+ if($tan == ""){
+ echo "No TAN found, waiting ".(60 - $i)."!\n";
+ continue;
+ }
+
+ break;
+ }
+
+ $unserialized = unserialize($serialized);
+
+ $fints = new FinTs(
+ FHP_BANK_URL,
+ FHP_BANK_PORT,
+ FHP_BANK_CODE,
+ FHP_ONLINE_BANKING_USERNAME,
+ FHP_ONLINE_BANKING_PIN,
+ null,
+ FHP_REGISTRATION_NO,
+ FHP_SOFTWARE_VERSION
+ );
+
+ $soa = $fints->finishStatementOfAccount($unserialized, $oneAccount, $from, $to, $tan);
+ }
+ $fints->end();
+
+} catch (\Exception $ex) {
+ echo 'Sth. went wrong - ' . $ex->getMessage();
+ exit;
+}
foreach ($soa->getStatements() as $statement) {
echo $statement->getDate()->format('Y-m-d') . ': Start Saldo: ' . ($statement->getCreditDebit() == Statement::CD_DEBIT ? '-' : '') . $statement->getStartBalance() . PHP_EOL;
@@ -40,7 +98,8 @@
echo 'Amount : ' . ($transaction->getCreditDebit() == Transaction::CD_DEBIT ? '-' : '') . $transaction->getAmount() . PHP_EOL;
echo 'Booking text: ' . $transaction->getBookingText() . PHP_EOL;
echo 'Name : ' . $transaction->getName() . PHP_EOL;
- echo 'Description : ' . $transaction->getDescription1() . PHP_EOL;
+ echo 'Description : ' . $transaction->getMainDescription() . PHP_EOL;
+ echo 'EREF : ' . $transaction->getEndToEndID() . PHP_EOL;
echo '=======================================' . PHP_EOL . PHP_EOL;
}
}
diff --git a/Samples/transfer.php b/Samples/transfer.php
new file mode 100644
index 0000000..c64f80a
--- /dev/null
+++ b/Samples/transfer.php
@@ -0,0 +1,128 @@
+add(new \DateInterval("P1D"));
+
+$sepaDD = new SEPATransfer(array(
+ 'messageID' => time(),
+ 'paymentID' => time()
+));
+
+$sepaDD->setDebitor(new SEPADebitor(array( //this is you
+ 'name' => 'My Company',
+ 'iban' => 'DE68210501700012345678',
+ 'bic' => 'DEUTDEDB400'#,
+ #'identifier' => 'DE98ZZZ09999999999'
+)));
+
+$sepaDD->addCreditor(new SEPACreditor(array( //this is who you want to send money to
+ #'paymentID' => '20170403652',
+ 'info' => '20170403652',
+ 'name' => 'Max Mustermann',
+ 'iban' => 'CH9300762011623852957',
+ 'bic' => 'GENODEF1P15',
+ 'amount' => 48.78,
+ 'currency' => 'EUR',
+ 'reqestedExecutionDate' => $dt
+)));
+
+use Fhp\FinTs;
+
+define('FHP_BANK_URL', ''); # HBCI / FinTS Url can be found here: https://www.hbci-zka.de/institute/institut_auswahl.htm (use the PIN/TAN URL)
+define('FHP_BANK_PORT', 443); # HBCI / FinTS Port can be found here: https://www.hbci-zka.de/institute/institut_auswahl.htm
+define('FHP_BANK_CODE', ''); # Your bank code / Bankleitzahl
+define('FHP_ONLINE_BANKING_USERNAME', ''); # Your online banking username / alias
+define('FHP_ONLINE_BANKING_PIN', ''); # Your online banking PIN (NOT! the pin of your bank card!)
+define('FHP_REGISTRATION_NO', ''); # The number you receive after registration / FinTS-Registrierungsnummer
+define('FHP_SOFTWARE_VERSION', '1.0'); # Your own Software product version
+
+$fints = new FinTs(
+ FHP_BANK_URL,
+ FHP_BANK_PORT,
+ FHP_BANK_CODE,
+ FHP_ONLINE_BANKING_USERNAME,
+ FHP_ONLINE_BANKING_PIN,
+ new testLogger(),
+ FHP_REGISTRATION_NO,
+ FHP_SOFTWARE_VERSION
+);
+
+$accounts = $fints->getSEPAAccounts();
+
+$fints->setTANMechanism(901); //901 for mobileTAN
+file_put_contents(__DIR__."/tan.txt", "");
+
+$oneAccount = $accounts[0];
+$transfer = $fints->executeSEPATransfer($oneAccount, $sepaDD->toXML(), function(){
+ return file_get_contents(__DIR__."/tan.txt");
+});
+
+$fints->end();
+print_r($transfer);
+
+//OR for web-applications without callback and interrupted connection
+/*
+
+$fints = new FinTs(
+ FHP_BANK_URL,
+ FHP_BANK_PORT,
+ FHP_BANK_CODE,
+ FHP_ONLINE_BANKING_USERNAME,
+ FHP_ONLINE_BANKING_PIN,
+ new testLogger()
+);
+
+$accounts = $fints->getSEPAAccounts();
+
+$fints->setTANMechanism(901); //901 for mobileTAN
+$response = $fints->executeSEPATransfer($accounts[0], $sepaDD->toXML());
+$serialized = serialize($response);
+
+echo "Waiting max. 60 seconds for TAN\n";
+
+for($i = 0; $i < 60; $i++){
+ sleep(1);
+
+ $tan = trim(file_get_contents(__DIR__."/tan.txt"));
+ if($tan == ""){
+ echo "No TAN found, waiting ".(60 - $i)."!\n";
+ continue;
+ }
+
+ break;
+}
+
+$unserialized = unserialize($serialized);
+
+$fints = new FinTs(
+ FHP_BANK_URL,
+ FHP_BANK_PORT,
+ FHP_BANK_CODE,
+ FHP_ONLINE_BANKING_USERNAME,
+ FHP_ONLINE_BANKING_PIN,
+ new testLogger()
+);
+ *
+$fints->finishSEPATAN($unserialized, $tan);
+$fints->end();
+ */
\ No newline at end of file
diff --git a/Samples/variables.php b/Samples/variables.php
new file mode 100644
index 0000000..4de6eef
--- /dev/null
+++ b/Samples/variables.php
@@ -0,0 +1,39 @@
+getVariables());
+$fints->end();
\ No newline at end of file
diff --git a/composer.json b/composer.json
index d2ca008..660cf85 100644
--- a/composer.json
+++ b/composer.json
@@ -1,8 +1,8 @@
{
- "name": "mschindler83/fints-hbci-php",
+ "name": "nemiah/php-fints",
"description": "PHP Library for the protocols fints and hbci",
- "homepage": "http://fints-hbci-php.markus-schindler.de",
- "version": "1.0.4",
+ "homepage": "https://github.com/nemiah/phpFinTS",
+ "version": "1.5.0",
"license": "MIT",
"autoload": {
"psr-0": {
@@ -10,13 +10,20 @@
}
},
"require": {
- "php": ">=5.3.2",
- "psr/log": "~1.0"
+ "php": ">=5.6",
+ "psr/log": "~1.0",
+ "ext-curl": "*"
},
"require-dev": {
- "phpunit/phpunit": ">=4.0,<=5.7"
+ "phpunit/phpunit": "5.7.*"
},
"suggest": {
- "monolog/monolog": "Allow sending log messages to a variety of different handlers"
+ "monolog/monolog": "Allow sending log messages to a variety of different handlers",
+ "nemiah/php-sepa-xml": "dev-master"
+ },
+ "scripts": {
+ "test": [
+ "phpunit"
+ ]
}
}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..8a18439
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,1457 @@
+{
+ "_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#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "df1c5584831bc613c8db5f900c1dd85a",
+ "packages": [
+ {
+ "name": "psr/log",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
+ "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "time": "2018-11-20T15:27:04+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "a2c590166b2133a4633738648b6b064edae0814a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a",
+ "reference": "a2c590166b2133a4633738648b6b064edae0814a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^6.0",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.13",
+ "phpstan/phpstan-phpunit": "^0.11",
+ "phpstan/phpstan-shim": "^0.11",
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "http://ocramius.github.com/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "time": "2019-03-17T17:37:11+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.9.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea",
+ "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "replace": {
+ "myclabs/deep-copy": "self.version"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.0",
+ "doctrine/common": "^2.6",
+ "phpunit/phpunit": "^7.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ },
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "time": "2019-08-09T12:45:53+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-common",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+ "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
+ "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~6"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "opensource@ijaap.nl"
+ }
+ ],
+ "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+ "homepage": "http://www.phpdoc.org",
+ "keywords": [
+ "FQSEN",
+ "phpDocumentor",
+ "phpdoc",
+ "reflection",
+ "static analysis"
+ ],
+ "time": "2018-08-07T13:53:10+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-docblock",
+ "version": "4.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+ "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
+ "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0",
+ "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
+ "phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
+ "webmozart/assert": "^1.0"
+ },
+ "require-dev": {
+ "doctrine/instantiator": "^1.0.5",
+ "mockery/mockery": "^1.0",
+ "phpunit/phpunit": "^6.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+ "time": "2019-09-12T14:27:41+00:00"
+ },
+ {
+ "name": "phpdocumentor/type-resolver",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/TypeResolver.git",
+ "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
+ "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1",
+ "phpdocumentor/reflection-common": "^2.0"
+ },
+ "require-dev": {
+ "ext-tokenizer": "^7.1",
+ "mockery/mockery": "~1",
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+ "time": "2019-08-22T18:11:29+00:00"
+ },
+ {
+ "name": "phpspec/prophecy",
+ "version": "1.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpspec/prophecy.git",
+ "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
+ "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.0.2",
+ "php": "^5.3|^7.0",
+ "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
+ "sebastian/comparator": "^1.1|^2.0|^3.0",
+ "sebastian/recursion-context": "^1.0|^2.0|^3.0"
+ },
+ "require-dev": {
+ "phpspec/phpspec": "^2.5|^3.2",
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Prophecy\\": "src/Prophecy"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ },
+ {
+ "name": "Marcello Duarte",
+ "email": "marcello.duarte@gmail.com"
+ }
+ ],
+ "description": "Highly opinionated mocking framework for PHP 5.3+",
+ "homepage": "https://github.com/phpspec/prophecy",
+ "keywords": [
+ "Double",
+ "Dummy",
+ "fake",
+ "mock",
+ "spy",
+ "stub"
+ ],
+ "time": "2019-06-13T12:50:23+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "4.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d",
+ "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-xmlwriter": "*",
+ "php": "^5.6 || ^7.0",
+ "phpunit/php-file-iterator": "^1.3",
+ "phpunit/php-text-template": "^1.2",
+ "phpunit/php-token-stream": "^1.4.2 || ^2.0",
+ "sebastian/code-unit-reverse-lookup": "^1.0",
+ "sebastian/environment": "^1.3.2 || ^2.0",
+ "sebastian/version": "^1.0 || ^2.0"
+ },
+ "require-dev": {
+ "ext-xdebug": "^2.1.4",
+ "phpunit/phpunit": "^5.7"
+ },
+ "suggest": {
+ "ext-xdebug": "^2.5.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sb@sebastian-bergmann.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "time": "2017-04-02T07:44:40+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "1.4.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4",
+ "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.4.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sb@sebastian-bergmann.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "time": "2017-11-27T13:52:08+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+ "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "time": "2015-06-21T13:50:34+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "1.0.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
+ "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.3 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sb@sebastian-bergmann.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "time": "2017-02-26T11:10:40+00:00"
+ },
+ {
+ "name": "phpunit/php-token-stream",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-token-stream.git",
+ "reference": "791198a2c6254db10131eecfe8c06670700904db"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db",
+ "reference": "791198a2c6254db10131eecfe8c06670700904db",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.2.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Wrapper around PHP's tokenizer extension.",
+ "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
+ "keywords": [
+ "tokenizer"
+ ],
+ "time": "2017-11-27T05:48:46+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "5.7.27",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c",
+ "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "myclabs/deep-copy": "~1.3",
+ "php": "^5.6 || ^7.0",
+ "phpspec/prophecy": "^1.6.2",
+ "phpunit/php-code-coverage": "^4.0.4",
+ "phpunit/php-file-iterator": "~1.4",
+ "phpunit/php-text-template": "~1.2",
+ "phpunit/php-timer": "^1.0.6",
+ "phpunit/phpunit-mock-objects": "^3.2",
+ "sebastian/comparator": "^1.2.4",
+ "sebastian/diff": "^1.4.3",
+ "sebastian/environment": "^1.3.4 || ^2.0",
+ "sebastian/exporter": "~2.0",
+ "sebastian/global-state": "^1.1",
+ "sebastian/object-enumerator": "~2.0",
+ "sebastian/resource-operations": "~1.0",
+ "sebastian/version": "^1.0.6|^2.0.1",
+ "symfony/yaml": "~2.1|~3.0|~4.0"
+ },
+ "conflict": {
+ "phpdocumentor/reflection-docblock": "3.0.2"
+ },
+ "require-dev": {
+ "ext-pdo": "*"
+ },
+ "suggest": {
+ "ext-xdebug": "*",
+ "phpunit/php-invoker": "~1.1"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.7.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "time": "2018-02-01T05:50:59+00:00"
+ },
+ {
+ "name": "phpunit/phpunit-mock-objects",
+ "version": "3.4.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
+ "reference": "a23b761686d50a560cc56233b9ecf49597cc9118"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118",
+ "reference": "a23b761686d50a560cc56233b9ecf49597cc9118",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.0.2",
+ "php": "^5.6 || ^7.0",
+ "phpunit/php-text-template": "^1.2",
+ "sebastian/exporter": "^1.2 || ^2.0"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.4"
+ },
+ "suggest": {
+ "ext-soap": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sb@sebastian-bergmann.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Mock Object library for PHPUnit",
+ "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
+ "keywords": [
+ "mock",
+ "xunit"
+ ],
+ "abandoned": true,
+ "time": "2017-06-30T09:13:00+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
+ "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7 || ^6.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "time": "2017-03-04T06:30:41+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "1.2.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
+ "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "sebastian/diff": "~1.2",
+ "sebastian/exporter": "~1.2 || ~2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "http://www.github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "time": "2017-01-29T09:50:25+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "1.4.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4",
+ "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.3 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.4-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff"
+ ],
+ "time": "2017-05-22T07:24:03+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac",
+ "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "time": "2016-11-26T07:53:53+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4",
+ "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "sebastian/recursion-context": "~2.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "~4.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "http://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "time": "2016-11-19T08:54:04+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4",
+ "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.2"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "time": "2015-10-12T03:26:01+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7",
+ "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6",
+ "sebastian/recursion-context": "~2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "time": "2017-02-18T15:18:39+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a",
+ "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+ "time": "2016-11-19T07:33:16+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
+ "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "time": "2015-07-28T20:34:47+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019",
+ "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "time": "2016-10-03T07:35:21+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "550ebaac289296ce228a706d0867afc34687e3f4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4",
+ "reference": "550ebaac289296ce228a706d0867afc34687e3f4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/yaml",
+ "version": "v4.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/yaml.git",
+ "reference": "5a0b7c32dc3ec56fd4abae8a4a71b0cf05013686"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/5a0b7c32dc3ec56fd4abae8a4a71b0cf05013686",
+ "reference": "5a0b7c32dc3ec56fd4abae8a4a71b0cf05013686",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "conflict": {
+ "symfony/console": "<3.4"
+ },
+ "require-dev": {
+ "symfony/console": "~3.4|~4.0"
+ },
+ "suggest": {
+ "symfony/console": "For validating YAML files using the lint command"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Yaml\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Yaml Component",
+ "homepage": "https://symfony.com",
+ "time": "2019-08-20T14:27:59+00:00"
+ },
+ {
+ "name": "webmozart/assert",
+ "version": "1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozart/assert.git",
+ "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4",
+ "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.3 || ^7.0",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.36 || ^7.5.13"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Assert\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Assertions to validate method input/output with nice error messages.",
+ "keywords": [
+ "assert",
+ "check",
+ "validate"
+ ],
+ "time": "2019-08-24T08:43:50+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=5.6",
+ "ext-curl": "*"
+ },
+ "platform-dev": []
+}
diff --git a/lib/Fhp/Adapter/AdapterInterface.php b/lib/Fhp/Adapter/AdapterInterface.php
deleted file mode 100644
index c78a507..0000000
--- a/lib/Fhp/Adapter/AdapterInterface.php
+++ /dev/null
@@ -1,18 +0,0 @@
-host = (string) $host;
- $this->port = (int) $port;
- $this->curlHandle = curl_init();
-
- curl_setopt($this->curlHandle, CURLOPT_SSLVERSION, 1);
- curl_setopt($this->curlHandle, CURLOPT_SSL_VERIFYPEER, true);
- curl_setopt($this->curlHandle, CURLOPT_SSL_VERIFYHOST, 2);
- curl_setopt($this->curlHandle, CURLOPT_USERAGENT, "FHP-lib");
- curl_setopt($this->curlHandle, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($this->curlHandle, CURLOPT_URL, $this->host);
- curl_setopt($this->curlHandle, CURLOPT_CONNECTTIMEOUT, 15);
- curl_setopt($this->curlHandle, CURLOPT_CUSTOMREQUEST, 'POST');
- curl_setopt($this->curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
- curl_setopt($this->curlHandle, CURLOPT_ENCODING, '');
- curl_setopt($this->curlHandle, CURLOPT_MAXREDIRS, 0);
- curl_setopt($this->curlHandle, CURLOPT_TIMEOUT, 30);
- curl_setopt($this->curlHandle, CURLOPT_HTTPHEADER, array("cache-control: no-cache", 'Content-Type: text/plain'));
- }
-
- /**
- * @param AbstractMessage $message
- * @return string
- * @throws CurlException
- */
- public function send(AbstractMessage $message)
- {
- curl_setopt($this->curlHandle, CURLOPT_POSTFIELDS, base64_encode($message->toString()));
- $response = curl_exec($this->curlHandle);
- $this->lastResponseInfo = curl_getinfo($this->curlHandle);
-
- if (false === $response) {
- throw new CurlException(
- 'Failed connection to ' . $this->host . ': ' . curl_error($this->curlHandle),
- curl_errno($this->curlHandle),
- null,
- $this->lastResponseInfo
- );
- }
-
- $statusCode = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE);
-
- if ($statusCode < 200 || $statusCode > 299) {
- throw new CurlException('Bad response with status code ' . $statusCode, 0, null, $this->lastResponseInfo);
- }
-
- return base64_decode($response);
- }
-
- /**
- * Gets curl info for last request / response.
- *
- * @return mixed
- */
- public function getLastResponseInfo()
- {
- return $this->lastResponseInfo;
- }
-}
diff --git a/lib/Fhp/Adapter/Debug.php b/lib/Fhp/Adapter/Debug.php
deleted file mode 100644
index ed5fb78..0000000
--- a/lib/Fhp/Adapter/Debug.php
+++ /dev/null
@@ -1,56 +0,0 @@
-host = (string) $host;
- $this->port = (int) $port;
- }
-
- /**
- * Should return a dummy response body.
- *
- * @param AbstractMessage $message
- * @return string
- */
- public function send(AbstractMessage $message)
- {
- /* @todo Implement me
- * return file_get_contents(__DIR__ . '/../../../develop/accounts_response.txt');
- */
- }
-}
diff --git a/lib/Fhp/Adapter/Exception/AdapterException.php b/lib/Fhp/Adapter/Exception/AdapterException.php
deleted file mode 100644
index 3cf7f80..0000000
--- a/lib/Fhp/Adapter/Exception/AdapterException.php
+++ /dev/null
@@ -1,12 +0,0 @@
-adapter = $adapter;
+ if (!is_integer($port) || (int) $port <= 0)
+ throw new CurlException('Invalid port number');
+
+
+ $this->host = (string) $host;
+ $this->port = (int) $port;
+ $this->timeoutConnect = (int) $timeoutConnect;
+ $this->timeResponse = (int) $timeoutResponse;
+
+ #$this->adapter = new Curl($server, $port, $timeout);
}
/**
- * Uses the configured adapter to send a message.
+ * Sends a message to the bank
*
* @param AbstractMessage $message
* @return string
+ * @throws CurlException
*/
public function send(AbstractMessage $message)
{
- return iconv('ISO-8859-1', 'UTF-8', $this->adapter->send($message));
+ return $this->sendCurl($message);
}
+
+
+ public function getCurlHandle(){
+ return $this->curlHandle;
+ }
+
+ private function connect(){
+ $this->curlHandle = curl_init();
+ curl_setopt($this->curlHandle, CURLOPT_SSLVERSION, 1);
+ curl_setopt($this->curlHandle, CURLOPT_SSL_VERIFYPEER, true);
+ curl_setopt($this->curlHandle, CURLOPT_SSL_VERIFYHOST, 2);
+ curl_setopt($this->curlHandle, CURLOPT_USERAGENT, "FHP-lib");
+ curl_setopt($this->curlHandle, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($this->curlHandle, CURLOPT_URL, $this->host);
+ curl_setopt($this->curlHandle, CURLOPT_CONNECTTIMEOUT, $this->timeoutConnect);
+ curl_setopt($this->curlHandle, CURLOPT_CUSTOMREQUEST, 'POST');
+ curl_setopt($this->curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
+ curl_setopt($this->curlHandle, CURLOPT_ENCODING, '');
+ curl_setopt($this->curlHandle, CURLOPT_MAXREDIRS, 0);
+ curl_setopt($this->curlHandle, CURLOPT_TIMEOUT, $this->timeResponse);
+ curl_setopt($this->curlHandle, CURLOPT_HTTPHEADER, array("cache-control: no-cache", 'Content-Type: text/plain'));
+ }
+
/**
- * @return AdapterInterface
+ * @param AbstractMessage $message
+ * @return string
+ * @throws CurlException
*/
- public function getAdapter()
- {
- return $this->adapter;
+ public function sendCurl(AbstractMessage $message) {
+ if(!$this->curlHandle)
+ $this->connect();
+
+ curl_setopt($this->curlHandle, CURLOPT_POSTFIELDS, base64_encode($message->toString()));
+ $response = curl_exec($this->curlHandle);
+ $this->lastResponseInfo = curl_getinfo($this->curlHandle);
+
+ if (false === $response) {
+ throw new CurlException(
+ 'Failed connection to ' . $this->host . ': ' . curl_error($this->curlHandle),
+ curl_errno($this->curlHandle),
+ null,
+ $this->lastResponseInfo
+ );
+ }
+
+ $statusCode = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE);
+
+ if ($statusCode < 200 || $statusCode > 299) {
+ throw new CurlException('Bad response with status code ' . $statusCode, 0, null, $this->lastResponseInfo);
+ }
+
+ return base64_decode($response);
+ }
+
+ /**
+ * Gets curl info for last request / response.
+ *
+ * @return mixed
+ */
+ public function getLastResponseInfo() {
+ return $this->lastResponseInfo;
}
}
diff --git a/lib/Fhp/Adapter/Exception/CurlException.php b/lib/Fhp/CurlException.php
similarity index 81%
rename from lib/Fhp/Adapter/Exception/CurlException.php
rename to lib/Fhp/CurlException.php
index 56b0a1b..6e51145 100644
--- a/lib/Fhp/Adapter/Exception/CurlException.php
+++ b/lib/Fhp/CurlException.php
@@ -1,12 +1,12 @@
connection = $connection;
- $this->bankCode = $bankCode;
- $this->username = $username;
- $this->pin = $pin;
- $this->systemId = $systemId;
- $this->logger = $logger;
- }
-
- /**
- * @param AbstractMessage $message
- * @return Response
- * @throws AdapterException
- * @throws CurlException
- * @throws FailedRequestException
- */
- public function sendMessage(AbstractMessage $message)
- {
- try {
- $this->logger->info('Sending Message');
- $message->setMessageNumber($this->messageNumber);
- $message->setDialogId($this->dialogId);
-
- $result = $this->connection->send($message);
- $this->messageNumber++;
- $response = new Response($result);
-
- $this->handleResponse($response);
-
- if (!$response->isSuccess()) {
- $summary = $response->getMessageSummary();
- $ex = new FailedRequestException($summary);
- $this->logger->error($ex->getMessage());
- throw $ex;
- }
-
- return $response;
- } catch (AdapterException $e) {
- $this->logger->critical($e->getMessage());
- if ($e instanceof CurlException) {
- $this->logger->debug(print_r($e->getCurlInfo(), true));
- }
-
- throw $e;
- }
- }
-
- /**
- * @param Response $response
- */
- protected function handleResponse(Response $response)
- {
- $summary = $response->getMessageSummary();
- $segSum = $response->getSegmentSummary();
-
- foreach ($summary as $code => $message) {
- $this->logMessage('HIRMG', $code, $message);
- }
-
- foreach ($segSum as $code => $message) {
- $this->logMessage('HIRMS', $code, $message);
- }
- }
-
- /**
- * @param string $type
- * @param string $code
- * @param $message
- */
- protected function logMessage($type, $code, $message)
- {
- switch (substr($code, 0, 1)) {
- case '0':
- $level = LogLevel::INFO;
- break;
- case "3":
- $level = LogLevel::WARNING;
- break;
- case "9":
- $level = LogLevel::ERROR;
- break;
- default:
- $level = LogLevel::INFO;
- }
-
- $this->logger->log($level, '[' . $type . '] ' . $message);
- }
-
- /**
- * Gets the dialog ID.
- *
- * @return integer
- */
- public function getDialogId()
- {
- return $this->dialogId;
- }
-
- /**
- * Gets the current message number.
- *
- * @return int
- */
- public function getMessageNumber()
- {
- return $this->messageNumber;
- }
-
- /**
- * Gets the system ID.
- *
- * @return int|string
- */
- public function getSystemId()
- {
- return $this->systemId;
- }
-
- /**
- * Gets all supported TAN mechanisms.
- *
- * @return array
- */
- public function getSupportedPinTanMechanisms()
- {
- return $this->supportedTanMechanisms;
- }
-
- /**
- * Gets the max possible HKSAL version.
- *
- * @return int
- */
- public function getHksalMaxVersion()
- {
- return $this->hksalVersion;
- }
-
- /**
- * Gets the max possible HKKAZ version.
- *
- * @return int
- */
- public function getHkkazMaxVersion()
- {
- return $this->hkkazVersion;
- }
-
- /**
- * Gets the bank name.
- *
- * @return string
- */
- public function getBankName()
- {
- return $this->bankName;
- }
-
- /**
- * Initializes a dialog.
- *
- * @return string|null
- * @throws AdapterException
- * @throws CurlException
- * @throws FailedRequestException
- * @throws \Exception
- */
- public function initDialog()
- {
- $this->logger->info('Initialize Dialog');
- $identification = new HKIDN(3, $this->bankCode, $this->username, $this->systemId);
- $prepare = new HKVVB(4, HKVVB::DEFAULT_BPD_VERSION, HKVVB::DEFAULT_UPD_VERSION, HKVVB::LANG_DEFAULT);
-
- $message = new Message(
- $this->bankCode,
- $this->username,
- $this->pin,
- $this->systemId,
- 0,
- 1,
- array($identification, $prepare),
- array(AbstractMessage::OPT_PINTAN_MECH => $this->supportedTanMechanisms)
- );
-
- $this->logger->debug('Sending INIT message: ' . (string) $message);
-
- $response = $this->sendMessage($message)->rawResponse;
- $this->logger->debug('Got INIT response: ' . $response);
-
- $result = new Initialization($response);
- $this->dialogId = $result->getDialogId();
- $this->logger->info('Received dialog ID: ' . $this->dialogId);
-
- return $this->dialogId;
- }
-
- /**
- * Sends sync request.
- *
- * @return string
- * @throws AdapterException
- * @throws CurlException
- * @throws FailedRequestException
- * @throws \Exception
- */
- public function syncDialog()
- {
- $this->logger->info('Initialize SYNC');
- $this->messageNumber = 1;
- $this->systemId = 0;
- $this->dialogId = 0;
-
- $identification = new HKIDN(3, $this->bankCode, $this->username, 0);
- $prepare = new HKVVB(4, HKVVB::DEFAULT_BPD_VERSION, HKVVB::DEFAULT_UPD_VERSION, HKVVB::LANG_DEFAULT);
- $sync = new HKSYN(5);
-
- $syncMsg = new Message(
- $this->bankCode,
- $this->username,
- $this->pin,
- $this->systemId,
- $this->dialogId,
- $this->messageNumber,
- array($identification, $prepare, $sync)
- );
-
- $this->logger->debug('Sending SYNC message: ' . (string) $syncMsg);
- $response = $this->sendMessage($syncMsg);
-
- $this->logger->debug('Got SYNC response: ' . $response->rawResponse);
-
- // save BPD (Bank Parameter Daten)
- $this->systemId = $response->getSystemId();
- $this->dialogId = $response->getDialogId();
- $this->bankName = $response->getBankName();
-
- // max version for segment HKSAL (Saldo abfragen)
- $this->hksalVersion = $response->getHksalMaxVersion();
- $this->supportedTanMechanisms = $response->getSupportedTanMechanisms();
-
- // max version for segment HKKAZ (Kontoumsätze anfordern / Zeitraum)
- $this->hkkazVersion = $response->getHkkazMaxVersion();
-
- $this->logger->info('Received system id: ' . $response->getSystemId());
- $this->logger->info('Received dialog id: ' . $response->getDialogId());
- $this->logger->info('Supported TAN mechanisms: ' . implode(', ', $this->supportedTanMechanisms));
-
- $this->endDialog();
-
- return $response->rawResponse;
- }
-
- /**
- * Ends a previous started dialog.
- *
- * @return string
- * @throws AdapterException
- * @throws CurlException
- * @throws FailedRequestException
- */
- public function endDialog()
- {
- $this->logger->info('Initialize END dialog message');
-
- $endMsg = new Message(
- $this->bankCode,
- $this->username,
- $this->pin,
- $this->systemId,
- $this->dialogId,
- $this->messageNumber,
- array(
- new HKEND(3, $this->dialogId)
- )
- );
-
- $this->logger->debug('Sending END message: ' . (string) $endMsg);
- $response = $this->sendMessage($endMsg);
- $this->logger->debug('Got END response: ' . $response->rawResponse);
-
- $this->logger->info('Resetting dialog ID and message number count');
- $this->dialogId = 0;
- $this->messageNumber = 1;
-
- return $response->rawResponse;
- }
+ const DEFAULT_COUNTRY_CODE = 280;
+
+ /**
+ * @var Connection
+ */
+ protected $connection;
+
+ /**
+ * @var LoggerInterface
+ */
+ protected $logger;
+
+ /**
+ * @var int
+ */
+ protected $messageNumber = 1;
+
+ /**
+ * @var int
+ */
+ protected $dialogId = 0;
+
+ /**
+ * @var int|string
+ */
+ protected $systemId = 0;
+
+ /**
+ * @var string
+ */
+ protected $bankCode;
+
+ /**
+ * @var string
+ */
+ protected $username;
+
+ /**
+ * @var string
+ */
+ protected $pin;
+
+ /**
+ * @var string
+ */
+ protected $bankName;
+
+ /**
+ * @var array
+ */
+ protected $supportedTanMechanisms = array();
+
+ /**
+ * @var int
+ */
+ protected $hksalVersion = 6;
+
+ /**
+ * @var int
+ */
+ protected $hkkazVersion = 6;
+
+ /**
+ * @var string
+ */
+ protected $productName;
+
+ /**
+ * @var string
+ */
+ protected $productVersion;
+
+ /**
+ * Dialog constructor.
+ *
+ * @param Connection $connection
+ * @param string $bankCode
+ * @param string $username
+ * @param string $pin
+ * @param string $systemId
+ * @param LoggerInterface $logger
+ * @param string $productName
+ * @param string $productVersion
+ */
+ public function __construct(
+ Connection $connection,
+ $bankCode,
+ $username,
+ $pin,
+ $systemId,
+ LoggerInterface $logger,
+ $productName,
+ $productVersion
+ ) {
+ $this->connection = $connection;
+ $this->bankCode = $bankCode;
+ $this->username = $username;
+ $this->pin = $pin;
+ $this->systemId = $systemId;
+ $this->logger = $logger;
+ $this->productName = $productName;
+ $this->productVersion = $productVersion;
+ }
+
+ /**
+ * @param AbstractMessage $message
+ * @param \Closure $tanCallback
+ * @param $interval
+ * @return Response|GetTANRequest
+ * @throws CurlException
+ * @throws FailedRequestException
+ */
+ public function sendMessage(AbstractMessage $message, $tanMechanism = null, \Closure $tanCallback = null, $interval = 1)
+ {
+ try {
+ $this->logger->debug('> '.$message);
+ $message->setMessageNumber($this->messageNumber);
+ $message->setDialogId($this->dialogId);
+
+ $result = $this->connection->send($message);
+ $this->messageNumber++;
+
+ $this->logger->debug('< '.$result);
+
+ $response = new Response($result, $this);
+ $this->handleResponse($response);
+ #$this->logger->info('Response reads:');
+ #$this->logger->info($response->rawResponse);
+
+ if (!$response->isSuccess()) {
+ $summaryS = $response->getSegmentSummary();
+ $summaryM = $response->getMessageSummary();
+
+ $summary = array();
+ foreach ($summaryS as $k => $v) {
+ $summary[$k] = $v;
+ }
+
+ foreach ($summaryM as $k => $v) {
+ if (isset($summary[$k])) {
+ $summary[$k] .= "($v)";
+ } else {
+ $summary[$k] = $v;
+ }
+ }
+
+ $ex = new FailedRequestException($summary);
+ $this->logger->error($ex->getMessage());
+ throw $ex;
+ }
+
+ $hitan = $response->splitSegment($response->findSegment('HITAN'));
+
+ if (!isset($hitan[4]) or $hitan[4] == 'nochallenge') {
+ return $response;
+ }
+
+ $response = new GetTANRequest($response->rawResponse, $this);
+
+ if (!$tanCallback) {
+ return $response;
+ }
+
+ $this->logger->info("Waiting max. 120 seconds for TAN from callback. Checking every $interval second(s)...");
+ for ($i = 0; $i < 120; $i += $interval) {
+ sleep($interval);
+
+ $tan = trim($tanCallback());
+ if ($tan == '') {
+ $this->logger->info('No TAN found, waiting '.(120 - $i).'!');
+ continue;
+ }
+
+ break;
+ }
+
+ if ($tan == '') {
+ throw new TANException('No TAN received!');
+ }
+
+ $response = $this->submitTAN($response, $tanMechanism, $tan);
+
+ return $response;
+ } catch (\Exception $e) {
+ $this->logger->critical($e->getMessage());
+ if ($e instanceof CurlException) {
+ $this->logger->debug(print_r($e->getCurlInfo(), true));
+ }
+
+ throw $e;
+ }
+ }
+
+ public function submitTAN($response, $tanMechanism, $tan)
+ {
+ $message = new Message(
+ $this->bankCode,
+ $this->username,
+ $this->pin,
+ $this->getSystemId(),
+ $this->getDialogId(),
+ $this->getMessageNumber(),
+ array(
+ new HKTAN(HKTAN::VERSION, 3, $response->get()->getProcessID())
+ ),
+ array(
+ AbstractMessage::OPT_PINTAN_MECH => $tanMechanism
+ ),
+ $tan
+ );
+
+ $this->logger->info('');
+ $this->logger->info('HKTAN (Zwei-Schritt-TAN-Einreichung) initialize');
+ $response = $this->sendMessage($message);
+ $this->logger->info('HKTAN end');
+
+ return $response;
+ }
+
+ /**
+ * @param Response $response
+ * @throws \Exception
+ */
+ protected function handleResponse(Response $response)
+ {
+ $summary = $response->getMessageSummary();
+ $segSum = $response->getSegmentSummary();
+
+ foreach ($summary as $code => $message) {
+ $this->logMessage('HIRMG', $code, $message);
+ }
+
+ foreach ($segSum as $code => $message) {
+ $this->logMessage('HIRMS', $code, $message);
+ }
+ #$this->logger->log(LogLevel::INFO, "");
+ }
+
+ /**
+ * @param string $type
+ * @param string $code
+ * @param $message
+ */
+ protected function logMessage($type, $code, $message)
+ {
+ switch (substr($code, 0, 1)) {
+ case '0':
+ $level = LogLevel::INFO;
+ break;
+ case '3':
+ $level = LogLevel::WARNING;
+ break;
+ case '9':
+ $level = LogLevel::ERROR;
+ break;
+ default:
+ $level = LogLevel::INFO;
+ }
+
+ $this->logger->log($level, '[' . $type . '] ' . $message);
+ }
+
+ /**
+ * Gets the dialog ID.
+ *
+ * @return integer
+ */
+ public function getDialogId()
+ {
+ return $this->dialogId;
+ }
+
+ /**
+ * Gets the current message number.
+ *
+ * @return int
+ */
+ public function getMessageNumber()
+ {
+ return $this->messageNumber;
+ }
+
+ /**
+ * Gets the system ID.
+ *
+ * @return int|string
+ */
+ public function getSystemId()
+ {
+ return $this->systemId;
+ }
+
+ /**
+ * Gets all supported TAN mechanisms.
+ *
+ * @return array
+ */
+ public function getSupportedPinTanMechanisms()
+ {
+ return $this->supportedTanMechanisms;
+ }
+
+ /**
+ * Gets the max possible HKSAL version.
+ *
+ * @return int
+ */
+ public function getHksalMaxVersion()
+ {
+ return $this->hksalVersion;
+ }
+
+ /**
+ * Gets the max possible HKKAZ version.
+ *
+ * @return int
+ */
+ public function getHkkazMaxVersion()
+ {
+ return $this->hkkazVersion;
+ }
+
+ /**
+ * Gets the bank name.
+ *
+ * @return string
+ */
+ public function getBankName()
+ {
+ return $this->bankName;
+ }
+
+ /**
+ * Initializes a dialog.
+ *
+ * @return string|null
+ * @throws CurlException
+ * @throws FailedRequestException
+ * @throws \Exception
+ */
+ public function initDialog()
+ {
+ $this->logger->info('');
+ $this->logger->info('DIALOG initialize');
+ $this->logger->debug('Registered product: ' . trim($this->productName . ' ' . $this->productVersion));
+
+ $identification = new HKIDN(3, $this->bankCode, $this->username, $this->systemId);
+ $prepare = new HKVVB(
+ 4,
+ HKVVB::DEFAULT_BPD_VERSION,
+ HKVVB::DEFAULT_UPD_VERSION,
+ HKVVB::LANG_DEFAULT,
+ $this->productName,
+ $this->productVersion
+ );
+
+ $message = new Message(
+ $this->bankCode,
+ $this->username,
+ $this->pin,
+ $this->systemId,
+ 0,
+ 1,
+ array(
+ $identification,
+ $prepare,
+ new HKTAN(HKTAN::VERSION, 5)
+ ),
+ array(AbstractMessage::OPT_PINTAN_MECH => $this->supportedTanMechanisms)
+ );
+
+ #$this->logger->debug('Sending INIT message:');
+ #$this->logger->debug((string) $message);
+
+ $response = $this->sendMessage($message)->rawResponse;
+ #$this->logger->debug('Got INIT response:');
+ #$this->logger->debug($response);
+
+ $result = new Initialization($response);
+ $this->dialogId = $result->getDialogId();
+ $this->logger->info('Received dialog ID: ' . $this->dialogId);
+
+ $this->logger->info('DIALOG end');
+
+ return $this->dialogId;
+ }
+
+ /**
+ * Sends sync request.
+ *
+ * @param boolean
+ * @return string
+ * @throws CurlException
+ * @throws FailedRequestException
+ * @throws \Exception
+ */
+ public function syncDialog($sendHKTan = true, $tanMechanism = null)
+ {
+ $this->logger->info('');
+ $this->logger->info('SYNC initialize');
+ $this->messageNumber = 1;
+ $this->systemId = 0;
+ $this->dialogId = 0;
+
+ $identification = new HKIDN(3, $this->bankCode, $this->username, 0);
+ $prepare = new HKVVB(
+ 4,
+ HKVVB::DEFAULT_BPD_VERSION,
+ HKVVB::DEFAULT_UPD_VERSION,
+ HKVVB::LANG_DEFAULT,
+ $this->productName,
+ $this->productVersion
+ );
+
+ if ($sendHKTan) {
+ $options = array();
+ if ($tanMechanism) {
+ $options[AbstractMessage::OPT_PINTAN_MECH] = array($tanMechanism);
+ }
+
+ $syncMsg = new Message(
+ $this->bankCode,
+ $this->username,
+ $this->pin,
+ $this->systemId,
+ $this->dialogId,
+ $this->messageNumber,
+ array(
+ $identification,
+ $prepare,
+ new HKTAN(HKTAN::VERSION, 5),
+ new HKSYN(6)
+ ),
+ $options
+ );
+ } else {
+ $syncMsg = new Message(
+ $this->bankCode,
+ $this->username,
+ $this->pin,
+ $this->systemId,
+ $this->dialogId,
+ $this->messageNumber,
+ array(
+ $identification,
+ $prepare,
+ new HKSYN(5)
+ )
+ );
+ }
+
+ #$this->logger->debug('Sending SYNC message:');
+ #$this->logger->debug((string) $syncMsg);
+ $response = $this->sendMessage($syncMsg);
+
+ #$this->checkResponse($response);
+
+ #$this->logger->debug('Received SYNC response:');
+ #$this->logger->debug($response->rawResponse);
+
+ // save BPD (Bank Parameter Daten)
+ $this->systemId = $response->getSystemId();
+ $this->dialogId = $response->getDialogId();
+ $this->bankName = $response->getBankName();
+
+ // max version for segment HKSAL (Saldo abfragen)
+ $this->hksalVersion = $response->getHksalMaxVersion();
+ $this->supportedTanMechanisms = $response->getSupportedTanMechanisms();
+
+ // max version for segment HKKAZ (Kontoumsätze anfordern / Zeitraum)
+ $this->hkkazVersion = $response->getHkkazMaxVersion();
+
+ $this->logger->info('Received system id: ' . $response->getSystemId());
+ $this->logger->info('Received dialog id: ' . $response->getDialogId());
+ $this->logger->info('Supported TAN mechanisms: ' . implode(', ', $this->supportedTanMechanisms));
+ $this->logger->info('SYNC end');
+
+ return $response;
+ }
+
+ /*public function checkResponse(Response $response){
+ foreach($response->getSegmentSummary() AS $k => $v)
+ if(substr($k, 0, 1) == "9")
+ throw new \Exception($v, $k);
+
+ }*/
+
+ /**
+ * Ends a previous started dialog.
+ *
+ * @return string
+ * @throws CurlException
+ * @throws FailedRequestException
+ */
+ public function endDialog()
+ {
+ $this->logger->info('');
+ $this->logger->info('END initialize');
+
+ $endMsg = new Message(
+ $this->bankCode,
+ $this->username,
+ $this->pin,
+ $this->systemId,
+ $this->dialogId,
+ $this->messageNumber,
+ array(
+ new HKEND(3, $this->dialogId)
+ )
+ );
+
+ #$this->logger->debug("S ".(string) $endMsg);
+ $response = null;
+ try {
+ $response = $this->sendMessage($endMsg);
+ } catch (FailedRequestException $ex) {
+ if (!$ex->isCodeSet(9800)) {
+ // 9800 is "Dialogabbruch", but we wanted to end it anyways
+ throw $ex;
+ }
+ $this->logger->warning('Dialog already ended: ' . $ex->getMessage());
+ }
+ #$this->logger->debug("R ".$response->rawResponse);
+
+ $this->logger->info('Resetting dialog ID and message number count');
+ $this->dialogId = 0;
+ $this->messageNumber = 1;
+
+ $this->logger->info('END end');
+
+ return null !== $response ? $response->rawResponse : null;
+ }
}
diff --git a/lib/Fhp/Dialog/Exception/FailedRequestException.php b/lib/Fhp/Dialog/Exception/FailedRequestException.php
index 31d4409..88eda6c 100644
--- a/lib/Fhp/Dialog/Exception/FailedRequestException.php
+++ b/lib/Fhp/Dialog/Exception/FailedRequestException.php
@@ -11,86 +11,95 @@
*/
class FailedRequestException extends \Exception
{
- /**
- * @var array
- */
- protected $summary = array();
+ /**
+ * @var array
+ */
+ protected $summary = array();
- /**
- * @var int
- */
- protected $responseCode = 0;
+ /**
+ * @var int
+ */
+ protected $responseCode = 0;
- /**
- * @var string
- */
- protected $responseMessage;
+ /**
+ * @var string
+ */
+ protected $responseMessage;
- /**
- * FailedRequestException constructor.
- *
- * @param array $summary
- */
- public function __construct(array $summary)
- {
- $this->summary = $summary;
- $keys = array_keys($summary);
-
- $this->responseCode = 0;
- $this->responseMessage = 'Unknown error';
-
- if (count($summary) == 1) {
- $this->responseCode = (int) $keys[0];
- $this->responseMessage = array_shift($summary);
- } elseif (count($summary) > 1) {
- foreach ($summary as $scode => $smsg) {
- if (0 === strpos($smsg, '*')) {
- $this->responseCode = (int) $scode;
- $this->responseMessage = substr($smsg, 1);
- }
- }
- }
-
- parent::__construct('Request Failed: ' . $this->responseMessage, $this->responseCode);
- }
+ /**
+ * FailedRequestException constructor.
+ *
+ * @param array $summary
+ */
+ public function __construct(array $summary)
+ {
+ $this->summary = $summary;
+ $keys = array_keys($summary);
- /**
- * @return array
- */
- public function getCodes()
- {
- return array_keys($this->summary);
- }
+ $this->responseCode = 0;
+ $this->responseMessage = 'Unknown error';
- /**
- * @return array
- */
- public function getSummary()
- {
- return $this->summary;
- }
+ if (count($summary) == 1) {
+ $this->responseCode = (int) $keys[0];
+ $this->responseMessage = array_shift($summary);
+ } elseif (count($summary) > 1) {
+ foreach ($summary as $scode => $smsg) {
+ $summary[$scode] = $summary[$scode]." ($scode)";
+ }
- /**
- * @return int
- */
- public function getResponseCode()
- {
- return $this->responseCode;
- }
- /**
- * @return string
- */
- public function getResponseMessage()
- {
- return $this->responseMessage;
- }
+ $this->responseMessage = implode('; ', $summary);
+ }
+
+ parent::__construct('Request Failed: ' . $this->responseMessage, $this->responseCode);
+ }
/**
- * @return string
- */
- public function getResponseMessages()
- {
- return implode(', ', $this->summary);
+ * Checks if the failed request contains the given error code.
+ *
+ * @param int $code
+ */
+ public function isCodeSet($code) {
+ return array_key_exists($code, $this->summary);
}
+
+ /**
+ * @return array
+ */
+ public function getCodes()
+ {
+ return array_keys($this->summary);
+ }
+
+ /**
+ * @return array
+ */
+ public function getSummary()
+ {
+ return $this->summary;
+ }
+
+ /**
+ * @return int
+ */
+ public function getResponseCode()
+ {
+ return $this->responseCode;
+ }
+
+ /**
+ * @return string
+ */
+ public function getResponseMessage()
+ {
+ return $this->responseMessage;
+ }
+
+ /**
+ * @return string
+ */
+ public function getResponseMessages()
+ {
+ return implode(', ', $this->summary);
+ }
}
diff --git a/lib/Fhp/Dialog/Exception/TANException.php b/lib/Fhp/Dialog/Exception/TANException.php
new file mode 100644
index 0000000..f3c89af
--- /dev/null
+++ b/lib/Fhp/Dialog/Exception/TANException.php
@@ -0,0 +1,15 @@
+server = $server;
- $this->port = $port;
+ $this->url = trim($server);
+ $this->port = intval($port);
$this->logger = null == $logger ? new NullLogger() : $logger;
// escaping of bank code not really needed here as it should
// never have special chars. But we just do it to ensure
// that the request will not get messed up and the user
// can receive a valid error response from the HBCI server.
- $this->bankCode = $this->escapeString($bankCode);
+ $this->bankCode = self::escapeString($bankCode);
// Here, escaping is needed for usernames or pins with
// HBCI special chars.
- $this->username = $this->escapeString($username);
- $this->pin = $this->escapeString($pin);
+ $this->username = self::escapeString($username);
+ $this->pin = self::escapeString($pin);
- $this->adapter = new Curl($this->server, $this->port);
- $this->connection = new Connection($this->adapter);
+ if ($productName != '') $this->productName = self::escapeString($productName);
+ if ($productVersion != '') $this->productVersion = self::escapeString($productVersion);
+
+ #$this->connection = new Connection($this->server, $this->port, $this->timeoutConnect, $this->timeoutResponse);
}
/**
- * Sets the adapter to use.
- *
- * @param AdapterInterface $adapter
+ * Sets the tan mechanism to use. Uses first found tan mechanism if not set.
+ * 901: mobileTAN
+ *
+ * @param int $mode
*/
- public function setAdapter(AdapterInterface $adapter)
- {
- $this->adapter = $adapter;
- $this->connection = new Connection($this->adapter);
- }
+ public function setTANMechanism($mode) {
+ $this->tanMechanism = $mode;
+ }
+
+ public function setTimeouts($connect, $response){
+ $this->timeoutConnect = $connect;
+ $this->timeoutResponse = $response;
+ }
/**
* Gets array of all accounts.
*
* @return Model\Account[]
*/
- public function getAccounts()
- {
- $dialog = $this->getDialog();
+ public function getAccounts() {
+ $dialog = $this->getDialog(false);
$result = $dialog->syncDialog();
+ $this->end();
$this->bankName = $dialog->getBankName();
$accounts = new GetAccounts($result);
@@ -117,37 +138,57 @@ public function getAccounts()
* Gets array of all SEPA Accounts.
*
* @return Model\SEPAAccount[]
- * @throws Adapter\Exception\AdapterException
- * @throws Adapter\Exception\CurlException
+ * @throws \CurlException
*/
- public function getSEPAAccounts()
- {
- $dialog = $this->getDialog();
- $dialog->syncDialog();
+ public function getSEPAAccounts() {
+ $dialog = $this->getDialog(false);#, $this->tanMechanism);
+ #$dialog->endDialog(); //probably not required
+ $dialog->syncDialog(true, $this->tanMechanism);
+ $dialog->endDialog();
$dialog->initDialog();
$message = $this->getNewMessage(
$dialog,
array(new HKSPA(3)),
- array(AbstractMessage::OPT_PINTAN_MECH => $dialog->getSupportedPinTanMechanisms())
+ array(AbstractMessage::OPT_PINTAN_MECH => $this->getUsedPinTanMechanism($dialog))
);
+ $this->logger->info('');
+ $this->logger->info('HKSPA (SEPA accounts) initialize');
$result = $dialog->sendMessage($message);
- $dialog->endDialog();
- $sepaAccounts = new GetSEPAAccounts($result->rawResponse);
+ $this->logger->info('HKSPA end');
+
+ $sepaAccounts = new GetSEPAAccounts($result->rawResponse);
return $sepaAccounts->getSEPAAccountsArray();
}
+ public function getVariables(){
+ $dialog = $this->getDialog(false);
+ $result = $dialog->syncDialog(false);
+ $this->end();
+
+ $R = new GetVariables($result->rawResponse);
+ return $R->get();
+ }
+
+ public function end(){
+ if(!$this->dialog)
+ return;
+
+ $this->dialog->endDialog();
+ $this->dialog = null;
+ }
+
/**
* Gets the bank name.
*
* @return string
*/
- public function getBankName()
- {
+ public function getBankName() {
if (null == $this->bankName) {
$this->getDialog()->syncDialog();
+ $this->end();
}
return $this->bankName;
@@ -159,24 +200,41 @@ public function getBankName()
* @param SEPAAccount $account
* @param \DateTime $from
* @param \DateTime $to
+ * @param \Closure $tanCallback
+ * @param $interval
* @return Model\StatementOfAccount\StatementOfAccount|null
* @throws \Exception
*/
- public function getStatementOfAccount(SEPAAccount $account, \DateTime $from, \DateTime $to)
- {
- $responses = array();
-
- $this->logger->info('Start fetching statement of account results');
+ public function getStatementOfAccount(SEPAAccount $account, \DateTime $from, \DateTime $to, \Closure $tanCallback = null, $interval = 1) {
+ $this->logger->info('');
+ $this->logger->info('HKKAZ (statement of accounts) initialize');
$this->logger->info('Start date: ' . $from->format('Y-m-d'));
$this->logger->info('End date : ' . $to->format('Y-m-d'));
$dialog = $this->getDialog();
- $dialog->syncDialog();
- $dialog->initDialog();
+ #$dialog->syncDialog();
+ #$dialog->initDialog();
$message = $this->createStateOfAccountMessage($dialog, $account, $from, $to, null);
- $response = $dialog->sendMessage($message);
- $touchdowns = $response->getTouchdowns($message);
+ $response = $dialog->sendMessage($message, $this->getUsedPinTanMechanism($dialog), $tanCallback, $interval);
+ #echo get_class($response);
+ if($response->isTANRequest())
+ return $response;
+
+ return $this->finishStatementOfAccount($response, $account, $from, $to);
+ }
+
+ public function finishStatementOfAccount(Response $response, SEPAAccount $account, \DateTime $from, \DateTime $to, $tan = null){
+ $dialog = $response->getDialog();
+ $this->dialog = $dialog;
+
+ if($tan)
+ $response = $dialog->submitTAN($response, $this->getUsedPinTanMechanism($dialog), $tan);
+
+ $message = $this->createStateOfAccountMessage($dialog, $account, $from, $to, null);
+
+ $responses = array();
+ $touchdowns = $response->getTouchdowns($message);
$soaResponse = new GetStatementOfAccount($response->rawResponse);
$responses[] = $soaResponse->getRawMt940();
@@ -188,7 +246,7 @@ public function getStatementOfAccount(SEPAAccount $account, \DateTime $from, \Da
$account,
$from,
$to,
- $touchdowns[HKKAZ::NAME]
+ self::escapeString($touchdowns[HKKAZ::NAME])
);
$r = $dialog->sendMessage($message);
@@ -197,129 +255,96 @@ public function getStatementOfAccount(SEPAAccount $account, \DateTime $from, \Da
$responses[] = $soaResponse->getRawMt940();
}
- $this->logger->info('Fetching of ' . $touchdownCounter . ' pages done.');
- $this->logger->debug('HKKAZ response:');
+ $this->logger->info('HKKAZ end');
- $dialog->endDialog();
+ $rawMt940 = implode('', $responses);
- return GetStatementOfAccount::createModelFromRawMt940(implode('', $responses));
- }
+
+ $urlParts = parse_url($this->url);
+ // Evtl. Groß und Kleinschreibungen des Hosts normalisieren
+ $dialectId = strtr($this->url, [
+ $urlParts['scheme'] => strtolower($urlParts['scheme']),
+ $urlParts['host'] => strtolower($urlParts['host']),
+ ]);
+
+ switch ($dialectId) {
+ case Parser\Dialect\SpardaMT940::DIALECT_ID:
+ $parser = new Parser\Dialect\SpardaMT940($rawMt940);
+ break;
+ case Parser\Dialect\PostbankMT940::DIALECT_ID:
+ $parser = new Parser\Dialect\PostbankMT940($rawMt940);
+ break;
+ default:
+ $parser = new MT940($rawMt940);
+ break;
+ }
+
+ return GetStatementOfAccount::createModelFromArray($parser->parse(MT940::TARGET_ARRAY));
+ }
/**
- * Helper method to create a "Statement of Account Message".
+ * Gets Bank To Customer Account Report as camt XML
*
- * @param Dialog $dialog
* @param SEPAAccount $account
* @param \DateTime $from
* @param \DateTime $to
- * @param string|null $touchdown
- * @return Message
+ * @return string[]
* @throws \Exception
*/
- protected function createStateOfAccountMessage(
- Dialog $dialog,
- SepaAccount $account,
- \DateTime $from,
- \DateTime $to,
- $touchdown = null
- ) {
- // version 4, 5, 6, 7
-
- // version 5
- /*
- 1 Segmentkopf DEG M 1
- 2 Kontoverbindung Auftraggeber DEG ktv # M 1
- 3 Alle Konten DE jn # M 1
- 4 Von Datum DE dat # K 1
- 5 Bis Datum DE dat # K 1
- 6 Maximale Anzahl Einträge DE num ..4 K 1 >0
- 7 Aufsetzpunkt DE an ..35 K 1
- */
-
- // version 6
- /*
- 1 Segmentkopf 1 DEG M 1
- 2 Kontoverbindung Auftraggeber 2 DEG ktv # M 1
- 3 Alle Konten 1 DE jn # M 1
- 4 Von Datum 1 DE dat # O 1
- 5 Bis Datum 1 DE dat # O 1
- 6 Maximale Anzahl Einträge 1 DE num ..4 C 1 >0
- 7 Aufsetzpunkt 1 DE an ..35 C 1
- */
-
- // version 7
- /*
- 1 Segmentkopf 1 DEG M 1
- 2 Kontoverbindung international 1 DEG kti # M 1
- 3 Alle Konten 1 DE jn # M 1
- 4 Von Datum 1 DE dat # O 1
- 5 Bis Datum 1 DE dat # O 1
- 6 Maximale Anzahl Einträge 1 DE num ..4 C 1 >0
- 7 Aufsetzpunkt 1 DE an ..35 C 1
- */
-
- switch ($dialog->getHkkazMaxVersion()) {
- case 4:
- case 5:
- $konto = new Deg();
- $konto->addDataElement($account->getAccountNumber());
- $konto->addDataElement($account->getSubAccount());
- $konto->addDataElement(static::DEFAULT_COUNTRY_CODE);
- $konto->addDataElement($account->getBlz());
- break;
- case 6:
- $konto = new Ktv(
- $account->getAccountNumber(),
- $account->getSubAccount(),
- new Kik(280, $account->getBlz())
- );
- break;
- case 7:
- $konto = new Kti(
- $account->getIban(),
- $account->getBic(),
- $account->getAccountNumber(),
- $account->getSubAccount(),
- new Kik(280, $account->getBlz())
- );
- break;
- default:
- throw new \Exception('Unsupported HKKAZ version: ' . $dialog->getHkkazMaxVersion());
+ public function getBankToCustomerAccountReportAsRawXML(SEPAAccount $account, \DateTime $from, \DateTime $to)
+ {
+ $responses = [];
+
+ $this->logger->info('');
+ $this->logger->info('HKCAZ (statement of accounts) initialize');
+ $this->logger->info('Start date: ' . $from->format('Y-m-d'));
+ $this->logger->info('End date : ' . $to->format('Y-m-d'));
+
+ $dialog = $this->getDialog();
+
+ $message = $this->createHKCAZMessage($dialog, $account, $from, $to, null);
+ $response = $dialog->sendMessage($message);
+ $touchdowns = $response->getTouchdowns($message);
+
+ $HICAZ = new BankToCustomerAccountReportHICAZ($response->rawResponse);
+ $responses[] = $HICAZ->getBookedXML();
+
+ $touchdownCounter = 1;
+ while (isset($touchdowns[HKCAZ::NAME])) {
+ $this->logger->info('Fetching more statement of account results (' . $touchdownCounter++ . ') ...');
+ $message = $this->createHKCAZMessage(
+ $dialog,
+ $account,
+ $from,
+ $to,
+ self::escapeString($touchdowns[HKCAZ::NAME])
+ );
+
+ $response = $dialog->sendMessage($message);
+ $touchdowns = $response->getTouchDowns($message);
+ $HICAZ = new BankToCustomerAccountReportHICAZ($response->rawResponse);
+ $responses[] = $HICAZ->getBookedXML();
}
- $message = $this->getNewMessage(
- $dialog,
- array(
- new HKKAZ(
- $dialog->getHkkazMaxVersion(),
- 3,
- $konto,
- HKKAZ::ALL_ACCOUNTS_N,
- $from,
- $to,
- $touchdown
- )
- ),
- array(AbstractMessage::OPT_PINTAN_MECH => $dialog->getSupportedPinTanMechanisms())
- );
+ $this->logger->info('HKCAZ end');
- return $message;
+ return $responses;
}
- /**
+ /**
* Gets the saldo of given SEPAAccount.
*
* @param SEPAAccount $account
* @return Model\Saldo|null
- * @throws Adapter\Exception\AdapterException
- * @throws Adapter\Exception\CurlException
+ * @throws \CurlException
* @throws \Exception
*/
- public function getSaldo(SEPAAccount $account)
- {
+ public function getSaldo(SEPAAccount $account) {
$dialog = $this->getDialog();
- $dialog->syncDialog();
- $dialog->initDialog();
+ #$dialog->syncDialog();
+ #$dialog->initDialog();
+
+ $addEncSegments = array();
switch ((int) $dialog->getHksalMaxVersion()) {
case 4:
@@ -333,6 +358,9 @@ public function getSaldo(SEPAAccount $account)
$hksalAccount->addDataElement($account->getSubAccount());
$hksalAccount->addDataElement(static::DEFAULT_COUNTRY_CODE);
$hksalAccount->addDataElement($account->getBlz());
+
+ $addEncSegments[] = new HKTAN(HKTAN::VERSION, 4);
+
break;
case 6:
$hksalAccount = new Ktv(
@@ -361,11 +389,12 @@ public function getSaldo(SEPAAccount $account)
$dialog->getSystemId(),
$dialog->getDialogId(),
$dialog->getMessageNumber(),
+ array_merge(
+ array(new HKSAL($dialog->getHksalMaxVersion(), 3, $hksalAccount, HKSAL::ALL_ACCOUNTS_N)),
+ $addEncSegments
+ ),
array(
- new HKSAL($dialog->getHksalMaxVersion(), 3, $hksalAccount, HKSAL::ALL_ACCOUNTS_N)
- ),
- array(
- AbstractMessage::OPT_PINTAN_MECH => $dialog->getSupportedPinTanMechanisms()
+ AbstractMessage::OPT_PINTAN_MECH => $this->getUsedPinTanMechanism($dialog)
)
);
@@ -375,60 +404,181 @@ public function getSaldo(SEPAAccount $account)
return $response->getSaldoModel();
}
- /**
- * Helper method to retrieve a pre configured message object.
- * Factory for poor people :)
- *
- * @param Dialog $dialog
- * @param array $segments
- * @param array $options
- * @return Message
- */
- protected function getNewMessage(Dialog $dialog, array $segments, array $options)
- {
- return new Message(
+ public function finishSEPATAN(GetTANRequest $tanRequest, $tan){
+ if($tan == "")
+ throw new TANException("No TAN received!");
+
+ $dialog = $tanRequest->getDialog();
+ $this->dialog = $dialog;
+
+ $dialog->submitTAN($tanRequest, $this->getUsedPinTanMechanism($dialog), $tan);
+ }
+
+ /**
+ * Executes SEPA transfer
+ * You have to call finishSEPATAN(), if $tanCallback is not set
+ *
+ * @param SEPAAccount $account
+ * @param string $painMessage
+ * @param \Closure $tanCallback
+ */
+ public function executeSEPATransfer(SEPAAccount $account, $painMessage, \Closure $tanCallback = null) {
+ $response = $this->startSEPATransfer($account, $painMessage);
+
+ if($tanCallback === null)
+ return $response;
+
+ echo "Waiting max. 120 seconds for TAN from callback\n";
+ for($i = 0; $i < 120; $i++){
+ sleep(1);
+
+ $tan = trim($tanCallback());
+ if($tan == ""){
+ echo "No TAN found, waiting ".(120 - $i)."!\n";
+ continue;
+ }
+
+ break;
+ }
+
+ $this->finishSEPATAN($response, $tan);
+ }
+
+ public function executeSEPADirectDebit(SEPAAccount $account, $painMessage, \Closure $tanCallback, $interval = 1) {
+ $painMessage = $this->clearXML($painMessage);
+
+
+ $dialog = $this->getDialog();
+ #$dialog->syncDialog();
+ #$dialog->initDialog();
+
+ $hkcdbAccount = new Kti(
+ $account->getIban(),
+ $account->getBic(),
+ $account->getAccountNumber(),
+ $account->getSubAccount(),
+ new Kik(280, $account->getBlz())
+ );
+
+ $hkdsx = new HKDSE(HKDSE::VERSION, 3, $hkcdbAccount, "urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.003.02", $painMessage);
+ if(strpos($painMessage, "COR1") !== false)
+ $hkdsx = new HKDSC(HKDSC::VERSION, 3, $hkcdbAccount, "urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.003.02", $painMessage);
+
+ $message = new Message(
$this->bankCode,
$this->username,
$this->pin,
$dialog->getSystemId(),
$dialog->getDialogId(),
$dialog->getMessageNumber(),
- $segments,
- $options
+ array(
+ $hkdsx,
+ new HKTAN(HKTAN::VERSION, 4)
+ ),
+ array(
+ AbstractMessage::OPT_PINTAN_MECH => $this->getUsedPinTanMechanism($dialog)
+ )
);
- }
- /**
- * Helper method to retrieve a pre configured dialog object.
- * Factory for poor people :)
- *
- * @return Dialog
- */
- protected function getDialog()
- {
- return new Dialog(
- $this->connection,
+ $class = explode("\\", get_class($hkdsx));
+ $this->logger->info('');
+ $this->logger->info($class[count($class) - 1].' (SEPA direct debit) initialize');
+ $response = $dialog->sendMessage($message);
+ $this->logger->info($class[count($class) - 1].' end');
+
+ $response = new GetTANRequest($response->rawResponse);
+ #print_r($response);
+
+ #var_dump($response->get()->getProcessID());
+ $this->logger->info("Waiting max. 120 seconds for TAN from callback. Checking every $interval second(s)...");
+ #echo "Waiting max. 120 seconds for TAN from callback. Checking every $interval second(s)…\n";
+ for($i = 0; $i < 120; $i += $interval){
+ sleep($interval);
+
+ $tan = trim($tanCallback());
+ if($tan == ""){
+ $this->logger->info("No TAN found, waiting ".(120 - $i)."!");
+ continue;
+ }
+
+ break;
+ }
+
+
+ if($tan == "")
+ throw new TANException("No TAN received!");
+
+ $dialog->submitTAN($response, $this->getUsedPinTanMechanism($dialog), $tan);
+ }
+
+ /**
+ * Executes SEPA delete standing order
+ *
+ * You have to call finishSEPATAN(), if $tanCallback is not set
+ *
+ * @param SEPAAccount $account
+ * @param SEPAStandingOrder $order
+ * @param \Closure $tanCallback
+ */
+ public function deleteSEPAStandingOrder(SEPAAccount $account, SEPAStandingOrder $order, \Closure $tanCallback = null) {
+ $response = $this->startDeleteSEPAStandingOrder($account, $order);
+
+ if($tanCallback === null)
+ return $response;
+
+ echo "Waiting max. 120 seconds for TAN from callback\n";
+ for($i = 0; $i < 120; $i++){
+ sleep(1);
+
+ $tan = trim($tanCallback());
+ if($tan == ""){
+ echo "No TAN found, waiting ".(120 - $i)."!\n";
+ continue;
+ }
+
+ break;
+ }
+
+ $this->finishSEPATAN($tan, $response);
+ }
+
+ public function getSEPAStandingOrders(SEPAAccount $account) {
+ $dialog = $this->getDialog();
+ #$dialog->syncDialog(false);
+ #$dialog->initDialog();
+
+ $hkcdbAccount = new Kti(
+ $account->getIban(),
+ $account->getBic(),
+ $account->getAccountNumber(),
+ $account->getSubAccount(),
+ new Kik(280, $account->getBlz())
+ );
+
+ $message = new Message(
$this->bankCode,
$this->username,
$this->pin,
- $this->systemId,
- $this->logger
+ $dialog->getSystemId(),
+ $dialog->getDialogId(),
+ $dialog->getMessageNumber(),
+ array(
+ new HKCDB(HKCDB::VERSION, 3, $hkcdbAccount, array("urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.03"))#, "pain.008.003.02.xsd"))
+ ),
+ array(
+ AbstractMessage::OPT_PINTAN_MECH => $this->getUsedPinTanMechanism($dialog)
+ )
);
- }
- /**
- * Needed for escaping userdata.
- * HBCI escape char is "?"
- *
- * @param string $string
- * @return string
- */
- protected function escapeString($string)
- {
- return str_replace(
- array('?', '@', ':', '+', '\''),
- array('??', '?@', '?:', '?+', '?\''),
- $string
- );
- }
+
+ $this->logger->info('');
+ $this->logger->info('HKCDB (SEPA standing orders) initialize');
+ $response = $dialog->sendMessage($message);
+
+ #$dialog->endDialog();
+ $this->logger->info('HKCDB end');
+ $response = new GetSEPAStandingOrders($response->rawResponse);
+
+ return $response->getSEPAStandingOrdersArray();
+ }
}
diff --git a/lib/Fhp/FinTsInternal.php b/lib/Fhp/FinTsInternal.php
new file mode 100644
index 0000000..1454e45
--- /dev/null
+++ b/lib/Fhp/FinTsInternal.php
@@ -0,0 +1,342 @@
+getDialog();
+ #$dialog->syncDialog();
+ #$dialog->initDialog();
+
+ $hkcdbAccount = new Kti(
+ $account->getIban(),
+ $account->getBic(),
+ $account->getAccountNumber(),
+ $account->getSubAccount(),
+ new Kik(280, $account->getBlz())
+ );
+
+ $message = new Message(
+ $this->bankCode,
+ $this->username,
+ $this->pin,
+ $dialog->getSystemId(),
+ $dialog->getDialogId(),
+ $dialog->getMessageNumber(),
+ array(
+ new HKCDL(HKCDL::VERSION, 3, $hkcdbAccount, "urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.03", $order),
+ new HKTAN(HKTAN::VERSION, 4)
+ ),
+ array(
+ AbstractMessage::OPT_PINTAN_MECH => $this->getUsedPinTanMechanism($dialog)
+ )
+ );
+
+ $response = $dialog->sendMessage($message);
+ return new GetTANRequest($response->rawResponse, $dialog);
+ }
+
+ /**
+ * @param SEPAAccount $account
+ * @param string $painMessage
+ * @return GetTANRequest
+ */
+ protected function startSEPATransfer(SEPAAccount $account, $painMessage) {
+ $painMessage = $this->clearXML($painMessage);
+
+ $dialog = $this->getDialog();
+ #$dialog->syncDialog();
+ #$dialog->initDialog();
+
+ $hkcdbAccount = new Kti(
+ $account->getIban(),
+ $account->getBic(),
+ $account->getAccountNumber(),
+ $account->getSubAccount(),
+ new Kik(280, $account->getBlz())
+ );
+
+ $message = new Message(
+ $this->bankCode,
+ $this->username,
+ $this->pin,
+ $dialog->getSystemId(),
+ $dialog->getDialogId(),
+ $dialog->getMessageNumber(),
+ array(
+ new HKCCS(HKCCS::VERSION, 3, $hkcdbAccount, "urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.03", $painMessage),
+ new HKTAN(HKTAN::VERSION, 4)
+ ),
+ array(
+ AbstractMessage::OPT_PINTAN_MECH => $this->getUsedPinTanMechanism($dialog)
+ )
+ );
+
+ $this->logger->info('');
+ $this->logger->info('HKCCS (SEPA Einzelüberweisung) initialize');
+ $response = $dialog->sendMessage($message);
+ $this->logger->info('HKCCS end');
+
+ return new GetTANRequest($response->rawResponse, $dialog);
+ }
+
+ /**
+ * Helper method to retrieve a pre configured message object.
+ * Factory for poor people :)
+ *
+ * @param Dialog $dialog
+ * @param array $segments
+ * @param array $options
+ * @return Message
+ */
+ protected function getNewMessage(Dialog $dialog, array $segments, array $options) {
+ return new Message(
+ $this->bankCode,
+ $this->username,
+ $this->pin,
+ $dialog->getSystemId(),
+ $dialog->getDialogId(),
+ $dialog->getMessageNumber(),
+ $segments,
+ $options
+ );
+ }
+
+ /**
+ * Helper method to retrieve a pre configured dialog object.
+ * Factory for poor people :)
+ *
+ * @param boolean
+ * @return Dialog
+ * @throws \Exception
+ */
+ protected function getDialog($sync = true) {
+ if ($this->dialog)
+ return $this->dialog;
+
+ if (!$this->connection)
+ $this->connection = new Connection($this->url, $this->port, $this->timeoutConnect, $this->timeoutResponse);
+
+ $D = new Dialog(
+ $this->connection,
+ $this->bankCode,
+ $this->username,
+ $this->pin,
+ $this->systemId,
+ $this->logger,
+ $this->productName,
+ $this->productVersion
+ );
+
+ if ($sync)
+ $D->syncDialog();
+
+ $this->dialog = $D;
+
+ return $this->dialog;
+ }
+
+ /**
+ * Needed for escaping userdata.
+ * HBCI escape char is "?"
+ *
+ * @param string $string
+ * @return string
+ */
+ public static function escapeString($string) {
+ return str_replace(
+ array('?', '@', ':', '+', '\''),
+ array('??', '?@', '?:', '?+', '?\''),
+ $string
+ );
+ }
+
+ protected function clearXML($xml) {
+ $dom = new \DOMDocument;
+ $dom->preserveWhiteSpace = FALSE;
+ $dom->loadXML($xml);
+ $dom->formatOutput = false;
+ return $dom->saveXml();
+ }
+
+ protected function getUsedPinTanMechanism($dialog) {
+ if($this->tanMechanism !== null AND in_array($this->tanMechanism, $dialog->getSupportedPinTanMechanisms()))
+ return array($this->tanMechanism);
+
+ return $dialog->getSupportedPinTanMechanisms();
+ }
+
+
+ /**
+ * Helper method to create a "Statement of Account Message".
+ *
+ * @param Dialog $dialog
+ * @param SEPAAccount $account
+ * @param \DateTime $from
+ * @param \DateTime $to
+ * @param string|null $touchdown
+ * @return Message
+ * @throws \Exception
+ */
+ protected function createHKCAZMessage(Dialog $dialog, SEPAAccount $account, \DateTime $from, \DateTime $to, $touchdown = null)
+ {
+ $kti = new Kti(
+ $account->getIban(),
+ $account->getBic(),
+ $account->getAccountNumber(),
+ $account->getSubAccount(),
+ new Kik(280, $account->getBlz())
+ );
+
+ $message = $this->getNewMessage(
+ $dialog,
+ array(
+ new HKCAZ(
+ 1,
+ 3,
+ $kti,
+ self::escapeString(HKCAZ::CAMT_FORMAT_FQ),
+ HKCAZ::ALL_ACCOUNTS_N,
+ $from,
+ $to,
+ $touchdown
+ )
+ ),
+ array(AbstractMessage::OPT_PINTAN_MECH => $this->getUsedPinTanMechanism($dialog))
+ );
+
+ return $message;
+ }
+
+ /**
+ * Helper method to create a "Statement of Account Message".
+ *
+ * @param Dialog $dialog
+ * @param SEPAAccount $account
+ * @param \DateTime $from
+ * @param \DateTime $to
+ * @param string|null $touchdown
+ * @return Message
+ * @throws \Exception
+ */
+ protected function createStateOfAccountMessage(
+ Dialog $dialog,
+ SepaAccount $account,
+ \DateTime $from,
+ \DateTime $to,
+ $touchdown = null
+ ) {
+ // version 4, 5, 6, 7
+
+ // version 5
+ /*
+ 1 Segmentkopf DEG M 1
+ 2 Kontoverbindung Auftraggeber DEG ktv # M 1
+ 3 Alle Konten DE jn # M 1
+ 4 Von Datum DE dat # K 1
+ 5 Bis Datum DE dat # K 1
+ 6 Maximale Anzahl Einträge DE num ..4 K 1 >0
+ 7 Aufsetzpunkt DE an ..35 K 1
+ */
+
+ // version 6
+ /*
+ 1 Segmentkopf 1 DEG M 1
+ 2 Kontoverbindung Auftraggeber 2 DEG ktv # M 1
+ 3 Alle Konten 1 DE jn # M 1
+ 4 Von Datum 1 DE dat # O 1
+ 5 Bis Datum 1 DE dat # O 1
+ 6 Maximale Anzahl Einträge 1 DE num ..4 C 1 >0
+ 7 Aufsetzpunkt 1 DE an ..35 C 1
+ */
+
+ // version 7
+ /*
+ 1 Segmentkopf 1 DEG M 1
+ 2 Kontoverbindung international 1 DEG kti # M 1
+ 3 Alle Konten 1 DE jn # M 1
+ 4 Von Datum 1 DE dat # O 1
+ 5 Bis Datum 1 DE dat # O 1
+ 6 Maximale Anzahl Einträge 1 DE num ..4 C 1 >0
+ 7 Aufsetzpunkt 1 DE an ..35 C 1
+ */
+
+ switch ($dialog->getHkkazMaxVersion()) {
+ case 4:
+ case 5:
+ $konto = new Deg();
+ $konto->addDataElement($account->getAccountNumber());
+ $konto->addDataElement($account->getSubAccount());
+ $konto->addDataElement(static::DEFAULT_COUNTRY_CODE);
+ $konto->addDataElement($account->getBlz());
+ break;
+ case 6:
+ $konto = new Ktv(
+ $account->getAccountNumber(),
+ $account->getSubAccount(),
+ new Kik(280, $account->getBlz())
+ );
+ break;
+ case 7:
+ $konto = new Kti(
+ $account->getIban(),
+ $account->getBic(),
+ $account->getAccountNumber(),
+ $account->getSubAccount(),
+ new Kik(280, $account->getBlz())
+ );
+ break;
+ default:
+ throw new \Exception('Unsupported HKKAZ version: ' . $dialog->getHkkazMaxVersion());
+ }
+
+ $message = $this->getNewMessage(
+ $dialog,
+ array(
+ new HKKAZ(
+ $dialog->getHkkazMaxVersion(),
+ 3,
+ $konto,
+ HKKAZ::ALL_ACCOUNTS_N,
+ $from,
+ $to,
+ $touchdown
+ ),
+ new HKTAN(6, 4)
+ ),
+ array(AbstractMessage::OPT_PINTAN_MECH => $this->getUsedPinTanMechanism($dialog))
+ );
+
+ return $message;
+ }
+}
diff --git a/lib/Fhp/Message/Message.php b/lib/Fhp/Message/Message.php
index 21eacfe..a3944bd 100644
--- a/lib/Fhp/Message/Message.php
+++ b/lib/Fhp/Message/Message.php
@@ -92,7 +92,8 @@ public function __construct(
$dialogId = 0,
$messageNumber = 0,
array $encryptedSegments = array(),
- array $options = array()
+ array $options = array(),
+ $tan = null
) {
$this->securityReference = rand(1000000, 9999999);
$this->dialogId = $dialogId;
@@ -105,7 +106,7 @@ public function __construct(
$this->profileVersion = SecurityProfile::PROFILE_VERSION_1;
$this->securityFunction = HNSHK::SECURITY_FUNC_999;
- if (isset($options[static::OPT_PINTAN_MECH]) && !empty($this->options[static::OPT_PINTAN_MECH])) {
+ if(isset($options[static::OPT_PINTAN_MECH])) {
if (!in_array('999', $this->options[static::OPT_PINTAN_MECH])) {
$this->profileVersion = SecurityProfile::PROFILE_VERSION_2;
$this->securityFunction = $this->options[static::OPT_PINTAN_MECH][0];
@@ -128,7 +129,7 @@ public function __construct(
$curCount = count($encryptedSegments) + 3;
- $signatureEnd = new HNSHA($curCount, $this->securityReference, $this->pin);
+ $signatureEnd = new HNSHA($curCount, $this->securityReference, $this->pin, $tan);
$this->addEncryptedSegment($signatureEnd);
$this->addSegment(new HNHBS($curCount + 1, $this->messageNumber));
}
@@ -193,4 +194,4 @@ public function getEncryptedSegments()
{
return $this->encryptedSegments;
}
-}
+}
\ No newline at end of file
diff --git a/lib/Fhp/Model/SEPAStandingOrder.php b/lib/Fhp/Model/SEPAStandingOrder.php
new file mode 100644
index 0000000..5f98a45
--- /dev/null
+++ b/lib/Fhp/Model/SEPAStandingOrder.php
@@ -0,0 +1,154 @@
+
+ */
+class SEPAStandingOrder
+{
+ /** @var string */
+ protected $id;
+ /** @var string */
+ protected $iban;
+ /** @var string */
+ protected $bic;
+ /** @var string */
+ protected $creditor;
+ /** @var float */
+ protected $amount;
+ /** @var string */
+ protected $xml;
+ /** @var string */
+ protected $firstExecution;
+ /** @var string */
+ protected $timeUnit;
+ /** @var string */
+ protected $interval;
+ /** @var string */
+ protected $executionDay;
+
+
+ public function setFirstExecution($date)
+ {
+ $this->firstExecution = (string) $date;
+
+ return $this;
+ }
+
+ public function setTimeUnit($u)
+ {
+ $this->timeUnit = (string) $u;
+
+ return $this;
+ }
+
+ public function setInterval($interval)
+ {
+ $this->interval = (string) $interval;
+
+ return $this;
+ }
+
+ public function setExecutionDay($day)
+ {
+ $this->executionDay = (string) $day;
+
+ return $this;
+ }
+
+ public function setXML($xml)
+ {
+ $this->xml = (string) $xml;
+
+ return $this;
+ }
+
+ public function setId($id)
+ {
+ $this->id = (string) $id;
+
+ return $this;
+ }
+
+ public function setCreditor($name)
+ {
+ $this->creditor = (string) $name;
+
+ return $this;
+ }
+
+ public function setIban($iban)
+ {
+ $this->iban = (string) $iban;
+
+ return $this;
+ }
+
+ public function setBic($bic)
+ {
+ $this->bic = (string) $bic;
+
+ return $this;
+ }
+
+ public function setAmount($amount)
+ {
+ $this->amount = (float) $amount;
+
+ return $this;
+ }
+
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ public function getCreditor()
+ {
+ return $this->creditor;
+ }
+
+ public function getIban()
+ {
+ return $this->iban;
+ }
+
+ public function getBic()
+ {
+ return $this->bic;
+ }
+
+ public function getAmount()
+ {
+ return $this->amount;
+ }
+
+ public function getXML()
+ {
+ return $this->xml;
+ }
+
+ public function getFirstExecution()
+ {
+ return $this->firstExecution;
+ }
+
+ public function getTimeUnit()
+ {
+ return $this->timeUnit;
+ }
+
+ public function getInterval()
+ {
+ return $this->interval;
+ }
+
+ public function getExecutionDay()
+ {
+ return $this->executionDay;
+ }
+}
diff --git a/lib/Fhp/Model/StatementOfAccount/StatementOfAccount.php b/lib/Fhp/Model/StatementOfAccount/StatementOfAccount.php
index 886abb4..302b966 100644
--- a/lib/Fhp/Model/StatementOfAccount/StatementOfAccount.php
+++ b/lib/Fhp/Model/StatementOfAccount/StatementOfAccount.php
@@ -37,6 +37,11 @@ public function setStatements(array $statements = null)
return $this;
}
+ public function isTANRequest()
+ {
+ return false;
+ }
+
/**
* @param Statement $statement
*/
diff --git a/lib/Fhp/Model/StatementOfAccount/Transaction.php b/lib/Fhp/Model/StatementOfAccount/Transaction.php
index a90c4d2..92449e7 100755
--- a/lib/Fhp/Model/StatementOfAccount/Transaction.php
+++ b/lib/Fhp/Model/StatementOfAccount/Transaction.php
@@ -10,6 +10,8 @@ class Transaction
{
const CD_CREDIT = 'credit';
const CD_DEBIT = 'debit';
+ const CD_CREDIT_CANCELLATION = 'credit_cancellation';
+ const CD_DEBIT_CANCELLATION = 'debit_cancellation';
/**
* @var \DateTime|null
@@ -31,6 +33,21 @@ class Transaction
*/
protected $creditDebit;
+ /**
+ * @var string
+ */
+ protected $turnoverDataRaw;
+
+ /**
+ * @var string
+ */
+ protected $multiPurposeDataRaw;
+
+ /**
+ * @var string
+ */
+ protected $bookingCode;
+
/**
* @var string
*/
@@ -67,6 +84,11 @@ class Transaction
*/
protected $name;
+ /**
+ * @var boolean
+ */
+ protected $booked;
+
/**
* Get booking date.
*
@@ -126,6 +148,21 @@ public function setValutaDate(\DateTime $date = null)
return $this;
}
+ /**
+ * Get the signed amount based on credit/debit setting.
+ * Debits and canceled credits have a negative sign.
+ * @return float
+ */
+ public function getSignedAmount()
+ {
+ switch ($this->creditDebit) {
+ case Transaction::CD_DEBIT:
+ case Transaction::CD_CREDIT_CANCELLATION:
+ return -1 * $this->amount;
+ default:
+ return $this->amount;
+ }
+ }
/**
* Get amount
@@ -137,6 +174,20 @@ public function getAmount()
return $this->amount;
}
+ /**
+ * Set booked status
+ *
+ * @param boolean $booked
+ *
+ * @return $this
+ */
+ public function setBooked($booked)
+ {
+ $this->booked = $booked;
+
+ return $this;
+ }
+
/**
* Set amount
*
@@ -175,6 +226,30 @@ public function setCreditDebit($creditDebit)
return $this;
}
+ /**
+ * Get bookingCode
+ *
+ * @return string
+ */
+ public function getBookingCode()
+ {
+ return $this->bookingCode;
+ }
+
+ /**
+ * Set bookingCode
+ *
+ * @param string $bookingCode
+ *
+ * @return $this
+ */
+ public function setBookingCode($bookingCode)
+ {
+ $this->bookingCode = (string) $bookingCode;
+
+ return $this;
+ }
+
/**
* Get bookingText
*
@@ -285,6 +360,20 @@ public function getMainDescription()
}
}
+ /**
+ * Get the end to end id (EREF)
+ *
+ * @return string
+ */
+ public function getEndToEndID()
+ {
+ if (array_key_exists('EREF', $this->structuredDescription)) {
+ return $this->structuredDescription['EREF'];
+ } else {
+ return "";
+ }
+ }
+
/**
* Get bankCode
*
@@ -343,17 +432,99 @@ public function getName()
return $this->name;
}
- /**
- * Set name
- *
- * @param string $name
- *
- * @return $this
- */
- public function setName($name)
- {
- $this->name = (string) $name;
-
- return $this;
- }
+ /**
+ * Set name
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = (string) $name;
+
+ return $this;
+ }
+
+ /**
+ * Get see https://www.bayernlb.de/internet/media/de/ir/downloads_1/zahlungsverkehr/formate_1/MT940_942.pdf page 451 / 8.2.6 Geschäftsvorfallcodes
+ *
+ * @return string
+ */
+ public function getTransactionCode()
+ {
+ return $this->transactionCode;
+ }
+
+ /**
+ * Set see https://www.bayernlb.de/internet/media/de/ir/downloads_1/zahlungsverkehr/formate_1/MT940_942.pdf page 451 / 8.2.6 Geschäftsvorfallcodes
+ *
+ * @param string $transactionCode See https://www.bayernlb.de/internet/media/de/ir/downloads_1/zahlungsverkehr/formate_1/MT940_942.pdf page 451 / 8.2.6 Geschäftsvorfallcodes
+ *
+ * @return self
+ */
+ public function setTransactionCode($transactionCode)
+ {
+ $this->transactionCode = $transactionCode;
+
+ return $this;
+ }
+
+ /**
+ * Get the value of turnoverDataRaw
+ *
+ * @return string
+ */
+ public function getTurnoverDataRaw()
+ {
+ return $this->turnoverDataRaw;
+ }
+
+ /**
+ * Set the value of turnoverDataRaw
+ *
+ * @param string $turnoverDataRaw
+ *
+ * @return self
+ */
+ public function setTurnoverDataRaw($turnoverDataRaw)
+ {
+ $this->turnoverDataRaw = $turnoverDataRaw;
+
+ return $this;
+ }
+
+ /**
+ * Get the value of multiPurposeDataRaw
+ *
+ * @return string
+ */
+ public function getMultiPurposeDataRaw()
+ {
+ return $this->multiPurposeDataRaw;
+ }
+
+ /**
+ * Set the value of multiPurposeDataRaw
+ *
+ * @param string $multiPurposeDataRaw
+ *
+ * @return self
+ */
+ public function setMultiPurposeDataRaw($multiPurposeDataRaw)
+ {
+ $this->multiPurposeDataRaw = $multiPurposeDataRaw;
+
+ return $this;
+ }
+
+ /**
+ * Get booked status
+ *
+ * @return boolean
+ */
+ public function getBooked()
+ {
+ return $this->booked;
+ }
}
diff --git a/lib/Fhp/Model/TANRequest.php b/lib/Fhp/Model/TANRequest.php
new file mode 100644
index 0000000..6a34da8
--- /dev/null
+++ b/lib/Fhp/Model/TANRequest.php
@@ -0,0 +1,27 @@
+
+ */
+class TANRequest
+{
+ /** @var string */
+ protected $processID;
+
+
+ public function setProcessID($processID)
+ {
+ $this->processID = (string) $processID;
+
+ return $this;
+ }
+
+ public function getProcessID()
+ {
+ return $this->processID;
+ }
+}
diff --git a/lib/Fhp/Parser/Dialect/PostbankMT940.php b/lib/Fhp/Parser/Dialect/PostbankMT940.php
new file mode 100644
index 0000000..25cfd56
--- /dev/null
+++ b/lib/Fhp/Parser/Dialect/PostbankMT940.php
@@ -0,0 +1,44 @@
+ implode("\n", $descriptionLines)
+ ];
+ }
+}
diff --git a/lib/Fhp/Parser/Dialect/SpardaMT940.php b/lib/Fhp/Parser/Dialect/SpardaMT940.php
new file mode 100644
index 0000000..f97a5ec
--- /dev/null
+++ b/lib/Fhp/Parser/Dialect/SpardaMT940.php
@@ -0,0 +1,92 @@
+ implode("\n", $correctedLines)
+ ];
+ } else {
+
+ // Beispiel
+ /*
+ 0 => "SEPA-BASISLASTSCHRIFT"
+ 1 => "EREF+ xxxxxxxxxxxxxxxxxx MR"
+ 2 => "EF+ xxxxxxxxxxxxxxxx CRED+"
+ 3 => "XXxxxxxxxxxxxxxxxxx SVWZ+ A"
+ 4 => "bcdef ghijklmn opqr stuvwxy"
+ 5 => "z1 1234 678912345"
+ 6 => "ABWA+ Abcd Efghij"
+ */
+
+ $combined = '';
+ foreach ($lines as $line) {
+ // Sonderfall, für Zeile 2 aus dem Beispiel
+ $combined .= preg_replace('/ ([A-Z]{4}\+)$/', " $1 ", $line);
+ }
+ $combined = implode("", $lines);
+
+ // SEPA Bezeichner müssen in einer neuen Zeile Anfangen und kein Leerzeichen hinter dem + haben
+ $fixed = preg_replace('/([A-Z]{4}\+) /', "\n$1", $combined);
+
+ $correctedLines = explode("\n", trim($fixed, "\n"));
+ }
+
+ // Buchungstext z.B. SEPA-ÜBERWEISUNG
+ if (count($otherInfo) > 0) {
+ $rawLines[0] = $bookingText = array_pop($otherInfo);
+
+ switch ($bookingText) {
+ case 'SEPA-ÜBERWEISUNG':
+ $gvc = '166';
+ case 'SEPA-BASISLASTSCHRIFT':
+ $gvc = '105';
+ //case 'SEPA-RÜCKLASTSCHRIFT':
+ // Hängt vom Betrag ab ?
+ //}
+ break;
+ }
+ }
+
+ // Rest vom Namen, wenn der > 27 Zeichen ist, ja ernsthaft
+ if (count($otherInfo) > 0) {
+ $rawLines[33] .= array_pop($otherInfo);
+ }
+
+ $desc = parent::extractStructuredDataFromRemittanceLines($correctedLines, $gvc, $rawLines);
+
+ if (isset($desc['SVWZ']) && strpos($desc['SVWZ'], 'Dauerauftrag') === 0) {
+ $gvc = '152';
+ $rawLines[0] = 'Dauerauftrag-Gutschrift';
+ }
+
+ return $desc;
+ }
+}
diff --git a/lib/Fhp/Parser/MT940.php b/lib/Fhp/Parser/MT940.php
index 5568c69..66fd268 100755
--- a/lib/Fhp/Parser/MT940.php
+++ b/lib/Fhp/Parser/MT940.php
@@ -6,228 +6,274 @@
/**
* Class MT940
+ *
+ * See https://www.kontopruef.de/mt940s.shtml for field documentation
+ *
* @package Fhp\Parser
*/
class MT940
{
- const TARGET_ARRAY = 0;
-
- const CD_CREDIT = 'credit';
- const CD_DEBIT = 'debit';
-
- /** @var string */
- protected $rawData;
- /** @var string */
- protected $soaDate;
-
- /**
- * MT940 constructor.
- *
- * @param string $rawData
- */
- public function __construct($rawData)
- {
- $this->rawData = (string) $rawData;
- }
-
- /**
- * @param string $target
- * @return array
- * @throws MT940Exception
- */
- public function parse($target)
- {
- switch ($target) {
- case static::TARGET_ARRAY:
- return $this->parseToArray();
- break;
- default:
- throw new MT940Exception('Invalid parse type provided');
- }
- }
-
- /**
- * @return array
- * @throws MT940Exception
- */
- protected function parseToArray()
- {
- // The divider can be either \r\n or @@
- $divider = substr_count($this->rawData, "\r\n-") > substr_count($this->rawData, '@@-') ? "\r\n" : '@@';
-
- $result = array();
- $days = preg_split('%' . $divider . '-$%', $this->rawData);
- foreach ($days as &$day) {
- $day = explode($divider . ':', $day);
- for ($i = 0, $cnt = count($day); $i < $cnt; $i++) {
- // handle start balance
- // 60F:C160401EUR1234,56
- if (preg_match('/^60(F|M):/', $day[$i])) {
- // remove 60(F|M): for better parsing
- $day[$i] = substr($day[$i], 4);
- $this->soaDate = $this->getDate(substr($day[$i], 1, 6));
-
- if (!isset($result[$this->soaDate])) {
- $result[$this->soaDate] = array('start_balance' => array());
- }
-
- $cdMark = substr($day[$i], 0, 1);
- if ($cdMark == 'C') {
- $result[$this->soaDate]['start_balance']['credit_debit'] = static::CD_CREDIT;
- } elseif ($cdMark == 'D') {
- $result[$this->soaDate]['start_balance']['credit_debit'] = static::CD_DEBIT;
- }
-
- $amount = str_replace(',', '.', substr($day[$i], 10));
- $result[$this->soaDate]['start_balance']['amount'] = $amount;
- } elseif (
- // found transaction
- // trx:61:1603310331DR637,39N033NONREF
- 0 === strpos($day[$i], '61:')
- && isset($day[$i + 1])
- && 0 === strpos($day[$i + 1], '86:')
- ) {
- $transaction = substr($day[$i], 3);
- $description = substr($day[$i + 1], 3);
-
- if (!isset($result[$this->soaDate]['transactions'])) {
- $result[$this->soaDate]['transactions'] = array();
- }
-
- // short form for better handling
- $trx = &$result[$this->soaDate]['transactions'];
-
- preg_match('/^\d{6}(\d{4})?(C|D|RC|RD)([A-Z]{1})?([^N]+)N/', $transaction, $trxMatch);
- if ($trxMatch[2] == 'C') {
- $trx[count($trx)]['credit_debit'] = static::CD_CREDIT;
- } elseif ($trxMatch[2] == 'D') {
- $trx[count($trx)]['credit_debit'] = static::CD_DEBIT;
- } else {
- throw new MT940Exception('cd mark not found in: ' . $transaction);
- }
-
- $amount = $trxMatch[4];
- $amount = str_replace(',', '.', $amount);
- $trx[count($trx) - 1]['amount'] = floatval($amount);
-
- $description = $this->parseDescription($description);
- $trx[count($trx) - 1]['description'] = $description;
-
- // :61:1605110509D198,02NMSCNONREF
- // 16 = year
- // 0511 = valuta date
- // 0509 = booking date
- $year = substr($transaction, 0, 2);
- $valutaDate = $this->getDate($year . substr($transaction, 2, 4));
-
- $bookingDate = substr($transaction, 6, 4);
- if (preg_match('/^\d{4}$/', $bookingDate)) {
- // if valuta date is earlier than booking date, then it must be in the new year.
- if (substr($transaction, 2, 2) == '12' && substr($transaction, 6, 2) == '01') {
- $year++;
- } elseif (substr($transaction, 2, 2) == '01' && substr($transaction, 6, 2) == '12') {
- $year--;
- }
- $bookingDate = $this->getDate($year . $bookingDate);
- } else {
- // if booking date not set in :61, then we have to take it from :60F
- $bookingDate = $this->soaDate;
- }
-
- $trx[count($trx) - 1]['booking_date'] = $bookingDate;
- $trx[count($trx) - 1]['valuta_date'] = $valutaDate;
- }
- }
- }
-
- return $result;
- }
-
- /**
- * @param string $descr
- * @return array
- */
- protected function parseDescription($descr)
- {
- $prepared = array();
- $result = array();
-
- // prefill with empty values
- for ($i = 0; $i <= 63; $i++) {
- $prepared[$i] = null;
- }
-
- $descr = str_replace("\r\n", '', $descr);
- $descr = str_replace('? ', '?', $descr);
- preg_match_all('/\?[\r\n]*(\d{2})([^\?]+)/', $descr, $matches, PREG_SET_ORDER);
-
- $descriptionLines = array();
- $description1 = ''; // Legacy, could be removed.
- $description2 = ''; // Legacy, could be removed.
- foreach ($matches as $m) {
- $index = (int) $m[1];
- if ((20 <= $index && $index <= 29) || (60 <= $index && $index <= 63)) {
- if (20 <= $index && $index <= 29) {
- $description1 .= $m[2];
- } else {
- $description2 .= $m[2];
- }
- $m[2] = trim($m[2]);
- if (!empty($m[2])) {
- $descriptionLines[] = $m[2];
- }
- } else {
- $prepared[$index] = $m[2];
- }
- }
-
- $description = array();
- if (empty($descriptionLines) || strlen($descriptionLines[0]) < 5 || $descriptionLines[0][4] !== '+') {
- $description['SVWZ'] = implode('', $descriptionLines);
- } else {
- $lastType = null;
- foreach ($descriptionLines as $line) {
- if (strlen($line) > 5 && $line[4] === '+') {
- if ($lastType != null) {
- $description[$lastType] = trim($description[$lastType]);
- }
- $lastType = substr($line, 0, 4);
- $description[$lastType] = substr($line, 5);
- } else {
- $description[$lastType] .= $line;
- }
- if (strlen($line) < 27) {
- // Usually, lines are 27 characters long. In case characters are missing, then it's either the end
- // of the current type or spaces have been trimmed from the end. We want to collapse multiple spaces
- // into one and we don't want to leave trailing spaces behind. So add a single space here to make up
- // for possibly missing spaces, and if it's the end of the type, it will be trimmed off later.
- $description[$lastType] .= ' ';
- }
- }
- $description[$lastType] = trim($description[$lastType]);
- }
-
- $result['description'] = $description;
- $result['booking_text'] = trim($prepared[0]);
- $result['primanoten_nr'] = trim($prepared[10]);
- $result['description_1'] = trim($description1);
- $result['bank_code'] = trim($prepared[30]);
- $result['account_number'] = trim($prepared[31]);
- $result['name'] = trim($prepared[32] . $prepared[33]);
- $result['text_key_addition'] = trim($prepared[34]);
- $result['description_2'] = trim($description2);
-
- return $result;
- }
-
- /**
- * @param string $val
- * @return string
- */
- protected function getDate($val)
- {
- $val = '20' . $val;
- preg_match('/(\d{4})(\d{2})(\d{2})/', $val, $m);
- return $m[1] . '-' . $m[2] . '-' . $m[3];
- }
+ const TARGET_ARRAY = 0;
+
+ const CD_CREDIT = 'credit';
+ const CD_DEBIT = 'debit';
+ const CD_CREDIT_CANCELLATION = 'credit_cancellation';
+ const CD_DEBIT_CANCELLATION = 'debit_cancellation';
+
+ // The divider can be either \r\n or @@
+ const LINE_DIVIDER = "(@@|\r\n)";
+
+ /** @var string */
+ protected $rawData;
+ /** @var string */
+ protected $soaDate;
+
+ /**
+ * MT940 constructor.
+ *
+ * @param string $rawData
+ */
+ public function __construct($rawData)
+ {
+ $this->rawData = (string) $rawData;
+ }
+
+ /**
+ * @param string $target
+ * @return array
+ * @throws MT940Exception
+ */
+ public function parse($target)
+ {
+ switch ($target) {
+ case static::TARGET_ARRAY:
+ return $this->parseToArray();
+ break;
+ default:
+ throw new MT940Exception('Invalid parse type provided');
+ }
+ }
+
+ /**
+ * @return array
+ * @throws MT940Exception
+ */
+ protected function parseToArray()
+ {
+ $result = array();
+
+ // split at every :20: ("Die Felder ":20:" bis ":28:" müssen vor jedem Zwischensaldo ausgegeben werden.")
+ $statementBlocks = preg_split('/' . self::LINE_DIVIDER . ':20:.*?' . self::LINE_DIVIDER . '/', $this->rawData);
+
+ foreach ($statementBlocks as $statementBlock) {
+ $parts = preg_split('/' . self::LINE_DIVIDER . ':/', $statementBlock);
+ $statement = array();
+ $transactions = array();
+ $cnt = 0;
+ for ($i = 0, $cnt = count($parts); $i < $cnt; $i++) {
+ // handle start balance
+ // 60F:C160401EUR1234,56
+ if (preg_match('/^60[FM]:/', $parts[$i])) {
+ $parts[$i] = substr($parts[$i], 4);
+ $this->soaDate = $this->getDate(substr($parts[$i], 1, 6));
+
+ $amount = str_replace(',', '.', substr($parts[$i], 10));
+ $statement['start_balance'] = array(
+ 'amount' => $amount,
+ 'credit_debit' => (substr($parts[$i], 0, 1) == 'C') ? static::CD_CREDIT : static::CD_DEBIT
+ );
+ $statement['date'] = $this->soaDate;
+ } elseif (
+ // found transaction
+ // trx:61:1603310331DR637,39N033NONREF
+ 0 === strpos($parts[$i], '61:')
+ && isset($parts[$i + 1])
+ && 0 === strpos($parts[$i + 1], '86:')
+ ) {
+ $transaction = substr($parts[$i], 3);
+ $description = substr($parts[$i + 1], 3);
+
+ $currentTrx = array();
+
+ $currentTrx['turnover_raw'] = ':' . $parts[$i];
+ $currentTrx['multi_purpose_raw'] = ':' . $parts[$i + 1];
+
+ preg_match('/^\d{6}(\d{4})?(C|D|RC|RD)[A-Z]?([^N]+)N/', $transaction, $matches);
+
+ switch ($matches[2]) {
+ case 'C':
+ $currentTrx['credit_debit'] = static::CD_CREDIT;
+ break;
+ case 'D':
+ $currentTrx['credit_debit'] = static::CD_DEBIT;
+ break;
+ case 'RC':
+ $currentTrx['credit_debit'] = static::CD_CREDIT_CANCELLATION;
+ break;
+ case 'RD':
+ $currentTrx['credit_debit'] = static::CD_DEBIT_CANCELLATION;
+ break;
+ default:
+ throw new MT940Exception('c/d/rc/rd mark not found in: ' . $transaction);
+ }
+
+ $amount = $matches[3];
+ $amount = str_replace(',', '.', $amount);
+ $currentTrx['amount'] = floatval($amount);
+
+ $currentTrx['transaction_code'] = substr($description, 0, 3);
+
+ $description = $this->parseDescription($description);
+ $currentTrx['description'] = $description;
+
+ // :61:1605110509D198,02NMSCNONREF
+ // 16 = year
+ // 0511 = valuta date
+ // 0509 = booking date
+ $year = substr($transaction, 0, 2);
+ $valutaDate = $this->getDate($year . substr($transaction, 2, 4));
+ $bookingDateMonthDay = substr($transaction, 6, 4);
+
+ $currentTrx['booking_date'] = $this->determineBookingDate($year, $valutaDate, $bookingDateMonthDay);
+ $currentTrx['valuta_date'] = $valutaDate;
+
+ $transactions[] = $currentTrx;
+ }
+ }
+ $statement['transactions'] = $transactions;
+ if (count($transactions) > 0 && array_key_exists('start_balance', $statement)) {
+ // $statement['start_balance'] is not set for earmarked transaction blocks
+ $result[] = $statement;
+ }
+ }
+
+ return $result;
+ }
+
+ protected function determineBookingDate($year, $valutaDate, $bookingDateMonthDay)
+ {
+ $result = null;
+ if (!preg_match('/^\d{4}$/', $bookingDateMonthDay)) {
+ // if booking date not set in :61, then we have to take it from :60F
+ return $this->soaDate;
+ }
+ // see https://github.com/mschindler83/fints-hbci-php/issues/101
+ $vd_date = new \DateTime($valutaDate);
+ $minDiff = null;
+ for ($bookingYear = $year - 1; $bookingYear <= $year + 1; $bookingYear++) {
+ $bd = $this->getDate($bookingYear . $bookingDateMonthDay);
+ $bd_date = new \DateTime($bd);
+ $diff = $bd_date->diff($vd_date)->days;
+ if ($diff === 0) {
+ return $bd; // shortcut
+ } elseif (null === $minDiff || $diff < $minDiff) {
+ $minDiff = $diff;
+ $result = $bd;
+ }
+ }
+ return $result;
+ }
+
+ protected function parseDescription($descr)
+ {
+ // Geschäftsvorfall-Code
+ $gvc = substr($descr, 0, 3);
+
+ $prepared = array();
+ $result = array();
+
+ // prefill with empty values
+ for ($i = 0; $i <= 63; $i++) {
+ $prepared[$i] = null;
+ }
+
+ $descr = preg_replace('/' . self::LINE_DIVIDER . '/', '', $descr);
+ $descr = preg_replace('/ +/', ' ', $descr);
+ $descr = str_replace('? ', '?', $descr);
+ preg_match_all('/\?[\r\n]*(\d{2})([^\?]+)/', $descr, $matches, PREG_SET_ORDER);
+
+ $descriptionLines = array();
+ $description1 = ''; // Legacy, could be removed.
+ $description2 = ''; // Legacy, could be removed.
+ foreach ($matches as $m) {
+ $index = (int) $m[1];
+ if ((20 <= $index && $index <= 29) || (60 <= $index && $index <= 63)) {
+ if (20 <= $index && $index <= 29) {
+ $description1 .= $m[2];
+ } else {
+ $description2 .= $m[2];
+ }
+ $m[2] = trim($m[2]);
+ if (!empty($m[2])) {
+ $descriptionLines[] = $m[2];
+ }
+ } else {
+ $prepared[$index] = $m[2];
+ }
+ }
+
+ $description = $this->extractStructuredDataFromRemittanceLines($descriptionLines, $gvc, $prepared);
+
+ $result['booking_code'] = $gvc;
+ $result['booking_text'] = trim($prepared[0]);
+ $result['description'] = $description;
+ $result['primanoten_nr'] = trim($prepared[10]);
+ $result['description_1'] = trim($description1);
+ $result['bank_code'] = trim($prepared[30]);
+ $result['account_number'] = trim($prepared[31]);
+ $result['name'] = trim($prepared[32] . $prepared[33]);
+ $result['text_key_addition'] = trim($prepared[34]);
+ $result['description_2'] = $description2;
+ $result['desc_lines'] = $descriptionLines;
+
+ return $result;
+ }
+
+ /**
+ * @param string[] $lines that contain the remittance information
+ * @param string $gvc Geschätsvorfallcode; Out-Parameter, might be changed from information in remittance info
+ * @param string $rawLines All the lines in the Multi-Purpose-Field 86; Out-Parameter, might be changed from information in remittance info
+ * @return array
+ */
+ protected function extractStructuredDataFromRemittanceLines($descriptionLines, &$gvc, &$rawLines)
+ {
+ $description = array();
+ if (empty($descriptionLines) || strlen($descriptionLines[0]) < 5 || $descriptionLines[0][4] !== '+') {
+ $description['SVWZ'] = implode('', $descriptionLines);
+ } else {
+ $lastType = null;
+ foreach ($descriptionLines as $line) {
+ if (strlen($line) >= 5 && $line[4] === '+') {
+ if ($lastType != null) {
+ $description[$lastType] = trim($description[$lastType]);
+ }
+ $lastType = substr($line, 0, 4);
+ $description[$lastType] = substr($line, 5);
+ } else {
+ $description[$lastType] .= $line;
+ }
+ if (strlen($line) < 27) {
+ // Usually, lines are 27 characters long. In case characters are missing, then it's either the end
+ // of the current type or spaces have been trimmed from the end. We want to collapse multiple spaces
+ // into one and we don't want to leave trailing spaces behind. So add a single space here to make up
+ // for possibly missing spaces, and if it's the end of the type, it will be trimmed off later.
+ $description[$lastType] .= ' ';
+ }
+ }
+ $description[$lastType] = trim($description[$lastType]);
+ }
+
+ return $description;
+ }
+
+ /**
+ * @param string $val
+ * @return string
+ */
+ protected function getDate($val)
+ {
+ $val = '20' . $val;
+ preg_match('/(\d{4})(\d{2})(\d{2})/', $val, $m);
+ return $m[1] . '-' . $m[2] . '-' . $m[3];
+ }
}
diff --git a/lib/Fhp/Response/BankToCustomerAccountReportHICAZ.php b/lib/Fhp/Response/BankToCustomerAccountReportHICAZ.php
new file mode 100644
index 0000000..95ea5c2
--- /dev/null
+++ b/lib/Fhp/Response/BankToCustomerAccountReportHICAZ.php
@@ -0,0 +1,48 @@
+findSegment(static::SEG_ACCOUNT_INFORMATION);
+
+ $parts = $this->splitSegment($seg);
+
+ if (count($parts) > 3) {
+
+ if ($parts[2] == HKCAZ::CAMT_FORMAT . '.xsd') {
+ list($empty, $length, $xml) = explode('@', $parts[3], 3);
+ if ($empty == '' && intval($length) == strlen($xml)) {
+ return $xml;
+ }
+
+ throw new \Exception('Fehler im XML Payload');
+
+ } else {
+ throw new \Exception('Unerwartetes CAMT XML Format (' . $parts[2] . ')');
+ }
+ }
+
+ return '';
+ }
+
+ protected function conformToUtf8($string)
+ {
+ return $string;
+ }
+}
diff --git a/lib/Fhp/Response/GetAccounts.php b/lib/Fhp/Response/GetAccounts.php
index d92e96e..bf260e8 100644
--- a/lib/Fhp/Response/GetAccounts.php
+++ b/lib/Fhp/Response/GetAccounts.php
@@ -24,10 +24,7 @@ public function getAccountsArray()
foreach ($accounts as $account) {
$accountParts = $this->splitSegment($account);
- $account = $this->createModelFromArray($accountParts);
- if ($account !== null) {
- $this->accounts[] = $account;
- }
+ $this->accounts[] = $this->createModelFromArray($accountParts);
}
return $this->accounts;
@@ -41,9 +38,6 @@ public function getAccountsArray()
*/
protected function createModelFromArray(array $array)
{
- if (!$array[1]) {
- return null;
- }
$account = new Account();
list($accountNumber, $x, $countryCode, $bankCode) = explode(':', $array[1]);
$account->setId($array[1]);
diff --git a/lib/Fhp/Response/GetSEPAStandingOrders.php b/lib/Fhp/Response/GetSEPAStandingOrders.php
new file mode 100644
index 0000000..2c94633
--- /dev/null
+++ b/lib/Fhp/Response/GetSEPAStandingOrders.php
@@ -0,0 +1,58 @@
+
+ */
+class GetSEPAStandingOrders extends Response
+{
+ const SEG_ACCOUNT_INFORMATION = 'HICDB';
+
+ /** @var array */
+ protected $orders = array();
+
+ /**
+ * Creates SEPA standing orders array list with SEPAStandingOrder models.
+ *
+ * @return SEPAStandingOrder[]
+ */
+ public function getSEPAStandingOrdersArray()
+ {
+ $segments = $this->findSegments(static::SEG_ACCOUNT_INFORMATION);
+
+ foreach($segments AS $segment){
+ $details = $this->splitSegment($segment, false);
+
+ $xml = preg_replace("/^@[0-9]+@/", "", $details[3]);
+ $sxml = new \SimpleXMLElement($xml);
+
+ $ex = explode(":", $details[5]);
+
+ foreach($sxml->CstmrCdtTrfInitn->PmtInf->CdtTrfTxInf AS $target){
+
+ $order = new SEPAStandingOrder();
+ $order->setCreditor($target->Cdtr->Nm);
+ $order->setIban($target->CdtrAcct->Id->IBAN);
+ $order->setBic($target->CdtrAgt->FinInstnId->BIC);
+ $order->setAmount($target->Amt->InstdAmt);
+ $order->setId($details[4]);
+ $order->setXML(preg_replace("/^@[0-9]+@/", "", $details[3]));
+ $order->setFirstExecution($ex[0]);
+ $order->setTimeUnit($ex[1]);
+ $order->setInterval($ex[2]);
+ $order->setExecutionDay($ex[3]);
+
+ $this->orders[] = $order;
+ }
+
+ }
+
+ return $this->orders;
+ }
+}
diff --git a/lib/Fhp/Response/GetStatementOfAccount.php b/lib/Fhp/Response/GetStatementOfAccount.php
index b1d720a..f16b478 100755
--- a/lib/Fhp/Response/GetStatementOfAccount.php
+++ b/lib/Fhp/Response/GetStatementOfAccount.php
@@ -54,24 +54,35 @@ public static function createModelFromRawMt940($rawMt940)
*/
protected static function addFromArray(array $array, StatementOfAccount $statementOfAccount)
{
- foreach ($array as $date => $statement) {
- if ($statementOfAccount->hasStatementForDate($date)) {
- $statementModel = $statementOfAccount->getStatementForDate($date);
- } else {
- $statementModel = new Statement();
- $statementModel->setDate(new \DateTime($date));
- $statementModel->setStartBalance((float) $statement['start_balance']['amount']);
- $statementModel->setCreditDebit($statement['start_balance']['credit_debit']);
- $statementOfAccount->addStatement($statementModel);
- }
+ foreach ($array as $statement) {
+ $statementModel = new Statement();
+ $statementModel->setDate(new \DateTime($statement['date']));
+ $statementModel->setStartBalance((float) $statement['start_balance']['amount']);
+ $statementModel->setCreditDebit($statement['start_balance']['credit_debit']);
+ $statementOfAccount->addStatement($statementModel);
if (isset($statement['transactions'])) {
foreach ($statement['transactions'] as $trx) {
+ $replaceIn = array(
+ 'booking_text',
+ 'description_1',
+ 'description_2',
+ 'description',
+ 'name'
+ );
+ foreach($replaceIn AS $k)
+ if(isset($trx['description'][$k]))
+ $trx['description'][$k] = str_replace ("@@", "", $trx['description'][$k]);
+
$transaction = new Transaction();
$transaction->setBookingDate(new \DateTime($trx['booking_date']));
$transaction->setValutaDate(new \DateTime($trx['valuta_date']));
+ $transaction->setTurnoverDataRaw($trx['turnover_raw']);
+ $transaction->setMultiPurposeDataRaw($trx['multi_purpose_raw']);
$transaction->setCreditDebit($trx['credit_debit']);
$transaction->setAmount($trx['amount']);
+ $transaction->setTransactionCode($trx['transaction_code']);
+ $transaction->setBookingCode($trx['description']['booking_code']);
$transaction->setBookingText($trx['description']['booking_text']);
$transaction->setDescription1($trx['description']['description_1']);
$transaction->setDescription2($trx['description']['description_2']);
@@ -79,6 +90,7 @@ protected static function addFromArray(array $array, StatementOfAccount $stateme
$transaction->setBankCode($trx['description']['bank_code']);
$transaction->setAccountNumber($trx['description']['account_number']);
$transaction->setName($trx['description']['name']);
+ $transaction->setBooked($trx['booked']);
$statementModel->addTransaction($transaction);
}
}
@@ -93,7 +105,7 @@ protected static function addFromArray(array $array, StatementOfAccount $stateme
* @param array $array
* @return StatementOfAccount
*/
- protected static function createModelFromArray(array $array)
+ public static function createModelFromArray(array $array)
{
$soa = static::addFromArray($array, new StatementOfAccount());
diff --git a/lib/Fhp/Response/GetTANRequest.php b/lib/Fhp/Response/GetTANRequest.php
new file mode 100644
index 0000000..b8d438b
--- /dev/null
+++ b/lib/Fhp/Response/GetTANRequest.php
@@ -0,0 +1,32 @@
+
+ */
+class GetTANRequest extends Response
+{
+ const SEG_ACCOUNT_INFORMATION = 'HITAN';
+
+ /**
+ * Returns TANRequest object with process ID
+ *
+ * @return TANRequest
+ */
+ public function get()
+ {
+ $segment = $this->findSegment(static::SEG_ACCOUNT_INFORMATION);
+ $details = $this->splitSegment($segment, false);
+ #print_r($details);
+ $request = new TANRequest();
+ $request->setProcessID($details[3]);
+
+ return $request;
+ }
+}
diff --git a/lib/Fhp/Response/GetVariables.php b/lib/Fhp/Response/GetVariables.php
new file mode 100644
index 0000000..ee82d02
--- /dev/null
+++ b/lib/Fhp/Response/GetVariables.php
@@ -0,0 +1,137 @@
+findSegments('HITANS');
+ $variables->tanModes = $this->parseTanModes($segments);
+ return $variables;
+ }
+
+ public function getSupportedTanMechanisms() {
+ return $this->get()->tanModes;
+ }
+
+ public function parseTanModes($segments)
+ {
+ // extracted from https://www.hbci-zka.de/dokumente/spezifikation_deutsch/fintsv3/FinTS_3.0_Security_Sicherheitsverfahren_PINTAN_2018-02-23_final_version.pdf
+ // Zwei-Schritt-TAN-Einreichung, Parameter
+ //
+ // 1. Segmentkopf:
+ // Beispiel: HITANS:169:6:4
+ // 1.1. Segmentkennung (HITANS)
+ // 1.2. Segmentnummer
+ // 1.3. Segmentversion
+ // 1.4. Bezugssegment
+ // 2. Maximale Anzahl Aufträge (int(3))
+ // 3. Anzahl Signaturen mindestens (0, 1, 2, 3)
+ // 4. Sicherheitsklasse (0, 1, 2, 3, 4)
+ // 5. Parameter Zwei-Schritt-TAN Einreichung
+ // 5.1. Einschrittverfahren erlaubt [jn] (V1-V6)
+ // 5.2. Mehr als ein TANpflichtiger Auftrag pro Nachricht erlaubt [jn] (V1-V6)
+ // 5.3. AuftragsHashwertverfahren [code] (V1-V6)
+ // 5.4. Sicherheitsprofil Banken-Signatur bei HITAN [code] (V1)
+ // 5.4. Verfahrensparameter ZweiSchritt-Verfahren (V2-V6)
+ // 5.5. Verfahrensparameter ZweiSchritt-Verfahren (V1)
+ // Sparkasse has 5 elements but declares version 6
+ $result = array();
+ foreach ($segments as $segmentRaw) {
+ $segment = $this->splitSegment($segmentRaw);
+
+ $segmentHeader = $this->splitDeg($segment[0]);
+ $version = (int) $segmentHeader[2]; // 1.3
+
+ $paramsIndex = count($segment) - 1;
+ $params = $this->splitDeg($segment[$paramsIndex]);
+
+ // 5. Parameter Zwei-Schritt-TAN-Einreichung:
+ // 5.4/5.5 s. Verfahrensparameter Zwei-Schritt-Verfahren
+ // Beispiel: J:N:0:910:2:HHD1.3.0:::chipTAN manuell:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:911:2:HHD1.3.2OPT:HHDOPT1:1.3.2:chipTAN optisch:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:912:2:HHD1.3.2USB:HHDUSB1:1.3.2:chipTAN-USB:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:913:2:Q1S:Secoder_UC:1.2.0:chipTAN-QR:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:920:2:smsTAN:::smsTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:2:N:5:921:2:pushTAN:::pushTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:2:N:2:900:2:iTAN:::iTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:0"
+ // -> Version 6
+ // J:N:0: (-> $processParamsOffset = 4)
+ // 910:2:HHD1.3.0:::chipTAN manuell:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:
+ // 911:2:HHD1.3.2OPT:HHDOPT1:1.3.2:chipTAN optisch:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:
+ // 912:2:HHD1.3.2USB:HHDUSB1:1.3.2:chipTAN-USB:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:
+ // 913:2:Q1S:Secoder_UC:1.2.0:chipTAN-QR:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:
+ // 920:2:smsTAN:::smsTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:2:N:5:
+ // 921:2:pushTAN:::pushTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:2:N:2:
+ // 900:2:iTAN:::iTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:0"
+
+ $processParamsOffset = $this->getTanProcessParamsOffsetForVersion($version);
+ $processNameIndex = $this->getTanProcessNameIndexForVersion($version);
+ $processParamElementCount = $this->getTanProcessParamElementCountForVersion($version);
+ array_splice($params, 0, $processParamsOffset);
+ if (count($params) % $processParamElementCount !== 0) {
+ throw new MT940Exception('Invalid number of params for HITANS version ' . $version . ': ' . count($params));
+ }
+ $paramBlockIterations = count($params) / $processParamElementCount;
+ for ($i = 0; $i < $paramBlockIterations; $i++) {
+ $blockOffset = $i * $processParamElementCount;
+ $num = $params[$blockOffset]; // first element in block
+ $name = $params[$processNameIndex + $blockOffset];
+ $result[$num] = $name;
+ }
+ }
+ return $result;
+ }
+
+ private function getTanProcessParamsOffsetForVersion($version)
+ {
+ switch ($version) {
+ case 1:
+ return 4;
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ return 3;
+ default:
+ throw new MT940Exception('Unknown HITANS version ' . $version);
+ }
+ }
+ private function getTanProcessParamElementCountForVersion($version)
+ {
+ switch ($version) {
+ case 1:
+ return 11;
+ break;
+ case 2:
+ return 15;
+ case 3:
+ return 18;
+ case 4:
+ case 5:
+ return 22;
+ case 6:
+ return 21;
+ default:
+ throw new MT940Exception('Unknown HITANS version ' . $version);
+ }
+ }
+ private function getTanProcessNameIndexForVersion($version)
+ {
+ switch ($version) {
+ case 1:
+ case 2:
+ case 3:
+ return 3;
+ case 4:
+ case 5:
+ case 6:
+ return 5;
+ default:
+ throw new MT940Exception('Unknown HITANS version ' . $version);
+ }
+ }
+}
diff --git a/lib/Fhp/Response/Response.php b/lib/Fhp/Response/Response.php
index 78ea043..46ae8a1 100644
--- a/lib/Fhp/Response/Response.php
+++ b/lib/Fhp/Response/Response.php
@@ -28,21 +28,37 @@ class Response
/** @var string */
protected $systemId;
+ private $dialog;
+
/**
* Response constructor.
*
* @param string $rawResponse
*/
- public function __construct($rawResponse)
+ public function __construct($rawResponse, \Fhp\Dialog\Dialog $dialog = null)
{
- if ($rawResponse instanceof Initialization) {
+ if ($rawResponse instanceof Response) {
$rawResponse = $rawResponse->rawResponse;
}
$this->rawResponse = $rawResponse;
$this->response = $this->unwrapEncryptedMsg($rawResponse);
+
+ $rawResponse = preg_replace("/\@([0-9]*)\@HIRMG/", "@$1@'HIRMG", $rawResponse);
$this->segments = preg_split("#'(?=[A-Z]{4,}:\d|')#", $rawResponse);
- }
+
+ $this->dialog = $dialog;
+ }
+
+ public function isTANRequest()
+ {
+ return get_class($this) == "Fhp\Response\GetTANRequest";
+ }
+
+ function getDialog()
+ {
+ return $this->dialog;
+ }
/**
* Extracts dialog ID from response.
@@ -161,6 +177,14 @@ public function isSuccess()
{
$summary = $this->getMessageSummary();
+ foreach ($summary as $code => $message) {
+ if ("9" == substr($code, 0, 1)) {
+ return false;
+ }
+ }
+
+ $summary = $this->getSegmentSummary();
+
foreach ($summary as $code => $message) {
if ("9" == substr($code, 0, 1)) {
return false;
@@ -194,23 +218,24 @@ public function getSegmentSummary()
* @return array
* @throws \Exception
*/
- protected function getSummaryBySegment($name)
- {
- if (!in_array($name, array('HIRMS', 'HIRMG'))) {
- throw new \Exception('Invalid segment for message summary. Only HIRMS and HIRMG supported');
- }
-
- $result = array();
- $segment = $this->findSegment($name);
- $segment = $this->splitSegment($segment);
- array_shift($segment);
- foreach ($segment as $de) {
- $de = $this->splitDeg($de);
- $result[$de[0]] = $de[2];
- }
-
- return $result;
- }
+ protected function getSummaryBySegment($name)
+ {
+ if (!in_array($name, array('HIRMS', 'HIRMG'))) {
+ throw new \Exception('Invalid segment for message summary. Only HIRMS and HIRMG supported');
+ }
+
+ $result = array();
+ foreach ($this->findSegments($name) AS $segment) {
+ $segment = $this->splitSegment($segment);
+ array_shift($segment);
+ foreach ($segment as $de) {
+ $de = $this->splitDeg($de);
+ $result[$de[0]] = $de[2];
+ }
+ }
+
+ return $result;
+ }
/**
* @param string $segmentName
@@ -290,7 +315,7 @@ protected function findSegmentForReference($name, AbstractSegment $reference)
*
* @return string|null
*/
- protected function findSegment($name)
+ public function findSegment($name)
{
return $this->findSegments($name, true);
}
@@ -308,6 +333,8 @@ protected function findSegments($name, $one = false)
foreach ($this->segments as $segment) {
$split = explode(':', $segment, 2);
+ $segment = $this->conformToUtf8($segment);
+
if ($split[0] == $name) {
if ($one) {
return $segment;
@@ -319,22 +346,33 @@ protected function findSegments($name, $one = false)
return $found;
}
+ protected function conformToUtf8($string)
+ {
+ return iconv('ISO-8859-1', 'UTF-8', $string);
+ }
+
/**
* @param $segment
*
* @return array
*/
- protected function splitSegment($segment)
+ public function splitSegment($segment, $fix = true)
{
+ preg_match("@\<\?xml.+Document\>@", $segment, $matches);
+ $segment = preg_replace("@\<\?xml.+Document\>@", "EXTRACTEDXML", $segment);
+
$parts = preg_split('/\+(? 0 AND isset($matches[0]))
+ $part = str_replace("EXTRACTEDXML", $matches[0], $part);
}
return $parts;
}
-
+
/**
* @param $deg
*
diff --git a/lib/Fhp/Segment/BaseDeg.php b/lib/Fhp/Segment/BaseDeg.php
new file mode 100644
index 0000000..18e2aaa
--- /dev/null
+++ b/lib/Fhp/Segment/BaseDeg.php
@@ -0,0 +1,60 @@
+descriptor = DegDescriptor::get(static::class);
+ }
+
+ public function getDescriptor()
+ {
+ return $this->descriptor;
+ }
+
+ /**
+ * @throws \InvalidArgumentException If any element in this DEG is invalid.
+ */
+ public function validate()
+ {
+ $this->descriptor->validateObject($this);
+ }
+
+ /**
+ * Short-hand for {@link Serializer#serializeDeg()}.
+ * @return string The HBCI wire format representation of this DEG, terminated by the segment delimiter.
+ */
+ public function serialize()
+ {
+ return Serializer::serializeDeg($this);
+ }
+
+ /**
+ * Convenience function for {@link Parser#parseGroup()}. This function should not be called on BaseDeg itself, but
+ * only on one of its sub-classes.
+ * @param string $rawElements The serialized wire format for a data element group.
+ * @return BaseDeg The parsed value.
+ */
+ public static function parse($rawElements)
+ {
+ return Parser::parseDeg($rawElements, static::class);
+ }
+}
diff --git a/lib/Fhp/Segment/BaseDescriptor.php b/lib/Fhp/Segment/BaseDescriptor.php
new file mode 100644
index 0000000..e64ad84
--- /dev/null
+++ b/lib/Fhp/Segment/BaseDescriptor.php
@@ -0,0 +1,186 @@
+isSubclassOf(BaseSegment::class) ? 1 : 0; // Segments have implicit Segmentkopf.
+ foreach ($clazz->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
+ if ($property->isStatic() || $property->getDeclaringClass()->name !== $clazz->name) {
+ // Skip static and super properties.
+ continue;
+ }
+
+ $docComment = $property->getDocComment();
+ if (!is_string($docComment)) {
+ throw new \InvalidArgumentException("Property $property must be annotated.");
+ }
+
+ $index = static::getIntAnnotation('Index', $docComment);
+ if ($index === null) {
+ if ($implicitIndex) {
+ $index = $nextIndex;
+ } else {
+ throw new \InvalidArgumentException("Property $property needs an explicit @Index");
+ }
+ } else {
+ // After one field was marked with an @Index, all subsequent fields need an explicit index too.
+ $implicitIndex = false;
+ }
+
+ $descriptor = new ElementDescriptor();
+ $descriptor->field = $property->getName();
+ $type = static::getVarAnnotation($docComment);
+ if (empty($type)) {
+ throw new \InvalidArgumentException("Need type on property $property");
+ }
+ $maxCount = static::getIntAnnotation('Max', $docComment);
+ if (substr($type, -5) === '|null') { // Nullable field
+ $descriptor->optional = true;
+ $type = substr($type, 0, -5);
+ }
+ if (substr($type, -2) === '[]') { // Array/repeated field
+ if ($maxCount === null) {
+ throw new \InvalidArgumentException("Repeated property $property needs @Max() annotation");
+ }
+ $descriptor->repeated = $maxCount;
+ $type = substr($type, 0, -2);
+ // If a repeated field is followed by anything at all, there will be an empty entry for each possible
+ // repeated value (in extreme cases, there can be hundreds of consecutive `+`, for instance).
+ $nextIndex += $maxCount;
+ } elseif ($maxCount !== null) {
+ throw new \InvalidArgumentException("@Max() annotation not recognized on single $property");
+ } else {
+ $nextIndex++; // Singular field, so the index advances by 1.
+ }
+ $descriptor->type = static::resolveType($type, $clazz);
+ $this->elements[$index] = $descriptor;
+ }
+ ksort($this->elements); // Make sure elements are parsed in wire-format order.
+ }
+
+ /**
+ * @param object $obj The object to be validated.
+ * @throws \InvalidArgumentException If any of the fields in the given object is not valid according to the schema
+ * defined by this descriptor.
+ */
+ public function validateObject($obj) {
+ if (!is_a($obj, $this->class)) {
+ throw new \InvalidArgumentException("Expected $this->class, got " . gettype($obj));
+ }
+ foreach ($this->elements as $elementDescriptor) {
+ $elementDescriptor->validateField($obj);
+ }
+ }
+
+ /**
+ * Looks for the annotation with the given name and extracts the content of the parentheses behind it. For instance,
+ * when called with the name "Index" and a docComment that contains {@}Index(15), this would return "15".
+ * @param string $name The name of the annotation.
+ * @param string $docComment The documentation string of a PHP field.
+ * @return string|null The content of the annotation, or null if absent.
+ */
+ private static function getAnnotation($name, $docComment)
+ {
+ $ret = preg_match("/@$name\\((.*?)\\)/", $docComment, $match);
+ if ($ret === false) {
+ throw new \RuntimeException("preg_match failed on $name");
+ }
+ return $ret === 1 ? $match[1] : null;
+ }
+
+ /**
+ * Same as above, with integer parsing.
+ * @param string $name The name of the annotation.
+ * @param string $docComment The documentation string of a PHP field.
+ * @return int|null The value of the annotation as an integer, or null if absent.
+ */
+ private static function getIntAnnotation($name, $docComment)
+ {
+ $val = static::getAnnotation($name, $docComment);
+ if ($val === null) {
+ return null;
+ }
+ if (!is_numeric($val)) {
+ throw new \InvalidArgumentException("Annotation $name has non-integer value $val");
+ }
+ return intval($val);
+ }
+
+ /**
+ * @param string $name The name of the annotation.
+ * @param string $docComment The documentation string of a PHP field.
+ * @return boolean Whether the annotation with the given name is present.
+ */
+ private static function getBoolAnnotation($name, $docComment)
+ {
+ return strpos("@$name", $docComment) !== false;
+ }
+
+ /**
+ * Separate parser for the {@}var` annotation because it does not use parentheses.
+ * @param string $docComment The documentation string of a PHP field.
+ * @return string|null The value of the {@}var annotation, or null if absent.
+ */
+ private static function getVarAnnotation($docComment)
+ {
+ $ret = preg_match("/@var ([^\\s]+)/", $docComment, $match);
+ if ($ret === false) {
+ throw new \RuntimeException("preg_match failed for @var");
+ }
+ return $ret === 1 ? $match[1] : null;
+ }
+
+ /**
+ * NOTE: This does *not* resolve `use` statements in the source file.
+ * @param string $typeName A type name (PHP class name, fully qualified or not) or a scalar type name.
+ * @param \ReflectionClass $contextClass The class where this type name was encountered, used for resolution of
+ * classes in the same package.
+ * @return string|\ReflectionClass The class that the type name refers to, or the scalar type name as a string.
+ */
+ private static function resolveType($typeName, $contextClass)
+ {
+ if (ElementDescriptor::isScalarType($typeName)) {
+ return $typeName;
+ }
+ if (strpos($typeName, '\\') === false) {
+ // Let's assume it's a relative type name, e.g. `X` mentioned in a file that starts with `namespace Fhp\Y`
+ // would become `\Fhp\X\Y`.
+ $typeName = $contextClass->getNamespaceName() . '\\' . $typeName;
+ }
+ try {
+ return new \ReflectionClass($typeName);
+ } catch (\ReflectionException $e) {
+ throw new \RuntimeException($e);
+ }
+ }
+}
diff --git a/lib/Fhp/Segment/BaseSegment.php b/lib/Fhp/Segment/BaseSegment.php
new file mode 100644
index 0000000..ed261f2
--- /dev/null
+++ b/lib/Fhp/Segment/BaseSegment.php
@@ -0,0 +1,85 @@
+v" where is the
+ * type of the segment (e.g. "HITANS") and is the numeric version. The *public* member fields of a sub-class
+ * determine the structure of the segment. The order matters for the wire format, whereas the field names are only used
+ * for documentation/readability purposes within this library. See {@link HITANSv1} for an example of a sub-class.
+ *
+ * @package Fhp\Segment
+ */
+abstract class BaseSegment implements SegmentInterface
+{
+
+ /**
+ * Reference to the descriptor for this type of segment.
+ * @var SegmentDescriptor
+ */
+ private $descriptor;
+
+ /**
+ * @var Segmentkopf
+ */
+ public $segmentkopf;
+
+ public function __construct()
+ {
+ $this->descriptor = SegmentDescriptor::get(static::class);
+ }
+
+ public function getDescriptor()
+ {
+ return $this->descriptor;
+ }
+
+ public function getName()
+ {
+ return $this->descriptor->kennung;
+ }
+
+ /**
+ * @throws \InvalidArgumentException If any element in this segment is invalid.
+ */
+ public function validate()
+ {
+ $this->descriptor->validateObject($this);
+ }
+
+ /**
+ * Short-hand for {@link Serializer#serializeSegment()}.
+ * @return string The HBCI wire format representation of this segment, terminated by the segment delimiter.
+ */
+ public function serialize()
+ {
+ return Serializer::serializeSegment($this);
+ }
+
+ // TODO Consider removing this along with SegmentInterface in future.
+ public function __toString()
+ {
+ return $this->serialize();
+ }
+
+ /**
+ * Convenience function for {@link Parser#parseSegment()}. This function should not be called on BaseSegment itself,
+ * but only on one of its sub-classes.
+ * @param string $rawSegment The serialized wire format for a single segment (segment delimiter may be present at
+ * the end, or not).
+ * @return BaseSegment The parsed segment.
+ */
+ public static function parse($rawSegment)
+ {
+ if (static::class === BaseSegment::class) {
+ throw new \BadFunctionCallException("Do not call BaseSegment::parse() on the super class");
+ }
+ return Parser::parseSegment($rawSegment, static::class);
+ }
+
+}
diff --git a/lib/Fhp/Segment/Common/Btg.php b/lib/Fhp/Segment/Common/Btg.php
new file mode 100644
index 0000000..b5b96d3
--- /dev/null
+++ b/lib/Fhp/Segment/Common/Btg.php
@@ -0,0 +1,13 @@
+class = $class;
+ try {
+ $clazz = new \ReflectionClass($class);
+ if (!$clazz->isSubclassOf(BaseDeg::class)) {
+ throw new \InvalidArgumentException("Must inherit from BaseDeg: $class");
+ }
+ parent::__construct($clazz);
+
+ // Check if the name ends in V2 or so, implicitly assume V1.
+ if (preg_match('/^[A-Z]+[vV]([0-9]+)$/', $clazz->getShortName(), $match) === 1) {
+ $this->version = intval($match[1]);
+ }
+ } catch (\ReflectionException $e) {
+ throw new \RuntimeException($e);
+ }
+ }
+}
diff --git a/lib/Fhp/Segment/ElementDescriptor.php b/lib/Fhp/Segment/ElementDescriptor.php
new file mode 100644
index 0000000..3aa7cf3
--- /dev/null
+++ b/lib/Fhp/Segment/ElementDescriptor.php
@@ -0,0 +1,110 @@
+$field does not correspond to the schema in this descriptor.
+ */
+ public function validateField($obj)
+ {
+ if (!isset($obj->{$this->field})) {
+ if ($this->optional) return;
+ throw new \InvalidArgumentException("Missing field $this->field");
+ }
+ $value = $obj->{$this->field};
+ if ($this->repeated) {
+ if (!is_array($value)) {
+ throw new \InvalidArgumentException("Expected array value for repeated field $this->field");
+ }
+ foreach ($value as $item) {
+ $this->validateValue($item);
+ }
+ } else {
+ $this->validateValue($value);
+ }
+ }
+
+ /**
+ * Maps types declared in a {@}var comment to the return format of `gettype()`.
+ */
+ const TYPE_MAP = [
+ 'int' => 'integer', 'integer' => 'integer',
+ 'float' => 'double',
+ 'bool' => 'boolean', 'boolean' => 'boolean',
+ 'string' => 'string',
+ ];
+
+ /**
+ * @param string $type A potential PHP scalar type.
+ * @return boolean True if parseDataElement() would understand it.
+ */
+ public static function isScalarType($type)
+ {
+ return array_key_exists($type, static::TYPE_MAP);
+ }
+
+ /**
+ * @param mixed $value The (non-null) value to be validated.
+ * @throws \InvalidArgumentException If $value is not a valid $type.
+ */
+ public function validateValue($value)
+ {
+ if (is_string($this->type) && array_key_exists($this->type, static::TYPE_MAP)) {
+ $expectedType = static::TYPE_MAP[$this->type];
+ $actualType = gettype($value);
+ if ($actualType !== $expectedType) {
+ throw new \InvalidArgumentException("Expected $expectedType, got $actualType: $value");
+ }
+ } elseif ($this->type instanceof \ReflectionClass) {
+ if (!$this->type->isInstance($value)) {
+ throw new \InvalidArgumentException("Expected {$this->type->name}, got $value");
+ }
+ if ($value instanceof BaseSegment || $value instanceof BaseDeg) {
+ $value->validate();
+ } else {
+ throw new \AssertionError(); // Violates guarantees of what we put in $this->type.
+ }
+ } else {
+ throw new \InvalidArgumentException("Unsupported type: $this->type");
+ }
+ }
+}
diff --git a/lib/Fhp/Segment/HITANS/HITANSv1.php b/lib/Fhp/Segment/HITANS/HITANSv1.php
new file mode 100644
index 0000000..5055819
--- /dev/null
+++ b/lib/Fhp/Segment/HITANS/HITANSv1.php
@@ -0,0 +1,27 @@
+ Nr. 9
+ * @package Fhp\Segment\HIUPD
+ */
+class ErlaubteGeschaeftsvorfaelleV1 extends BaseDeg
+{
+ /** @var string References a segment type name (Segmentkennung) */
+ public $geschaeftsvorfall;
+ /** @var integer Allowed values: 0, 1, 2, 3 */
+ public $anzahlBenoetigterSignaturen;
+ /** @var string|null Allowed values: E, T, W, M, Z */
+ public $limitart;
+ /** @var \Fhp\Segment\Common\Btg|null */
+ public $limitbetrag;
+ /** @var integer|null If present, must be greater than 0 */
+ public $limitTage;
+}
diff --git a/lib/Fhp/Segment/HIUPD/HIUPDv4.php b/lib/Fhp/Segment/HIUPD/HIUPDv4.php
new file mode 100644
index 0000000..b17c2e2
--- /dev/null
+++ b/lib/Fhp/Segment/HIUPD/HIUPDv4.php
@@ -0,0 +1,37 @@
+ Nr. 8
+ * @package Fhp\Segment\HIUPD
+ */
+class KontolimitV1 extends BaseDeg
+{
+ /** @var string Allowed values: E, T, W, M, Z */
+ public $limitart;
+ /** @var \Fhp\Segment\Common\Btg */
+ public $limitbetrag;
+ /** @var integer|null If present, must be greater than 0 */
+ public $limitTage;
+}
diff --git a/lib/Fhp/Segment/HIUPD/KtvV3.php b/lib/Fhp/Segment/HIUPD/KtvV3.php
new file mode 100644
index 0000000..81f0f66
--- /dev/null
+++ b/lib/Fhp/Segment/HIUPD/KtvV3.php
@@ -0,0 +1,29 @@
+
+ * @package Fhp\Segment
+ */
+class HKCCS extends AbstractSegment
+{
+ const NAME = 'HKCCS';
+ const VERSION = 1;
+
+ /**
+ * HKCCS constructor.
+ * @param int $version
+ * @param int $segmentNumber
+ * @param Kti $kti
+ * @param string $SEPADescriptor
+ * @param string $painMessage
+ */
+ public function __construct($version, $segmentNumber, $kti, $SEPADescriptor, $painMessage)
+ {
+ parent::__construct(
+ static::NAME,
+ $segmentNumber,
+ $version,
+ array(
+ $kti,
+ $SEPADescriptor,
+ new Bin($painMessage)
+ )
+ );
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return static::NAME;
+ }
+}
diff --git a/lib/Fhp/Segment/HKCDB.php b/lib/Fhp/Segment/HKCDB.php
new file mode 100644
index 0000000..ee1075c
--- /dev/null
+++ b/lib/Fhp/Segment/HKCDB.php
@@ -0,0 +1,53 @@
+
+ * @package Fhp\Segment
+ */
+class HKCDB extends AbstractSegment
+{
+ const NAME = 'HKCDB';
+ const VERSION = 1;
+
+ /**
+ * HKCDB constructor.
+ * @param int $version
+ * @param int $segmentNumber
+ * @param Kti $kti
+ * @param array $supportedPain
+ */
+ public function __construct($version, $segmentNumber, $kti, $supportedPain)
+ {
+ $deg = new Deg();
+ foreach ($supportedPain AS $pain)
+ $deg->addDataElement($pain);
+
+ parent::__construct(
+ static::NAME,
+ $segmentNumber,
+ $version,
+ array(
+ $kti,
+ $deg
+ )
+ );
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return static::NAME;
+ }
+}
diff --git a/lib/Fhp/Segment/HKCDL.php b/lib/Fhp/Segment/HKCDL.php
new file mode 100644
index 0000000..e0a12d8
--- /dev/null
+++ b/lib/Fhp/Segment/HKCDL.php
@@ -0,0 +1,61 @@
+
+ * @package Fhp\Segment
+ */
+class HKCDL extends AbstractSegment
+{
+ const NAME = 'HKCDL';
+ const VERSION = 1;
+
+ /**
+ * HKCDL constructor.
+ * @param int $version
+ * @param int $segmentNumber
+ * @param Kti $kti
+ * @param string $SEPADescriptor
+ * @param SEPAStandingOrder $SEPAStandingOrder
+ */
+ public function __construct($version, $segmentNumber, $kti, $SEPADescriptor, SEPAStandingOrder $SEPAStandingOrder)
+ {
+ $deg = new \Fhp\Deg();
+ $deg->addDataElement($SEPAStandingOrder->getFirstExecution());
+ $deg->addDataElement($SEPAStandingOrder->getTimeUnit());
+ $deg->addDataElement($SEPAStandingOrder->getInterval());
+ $deg->addDataElement($SEPAStandingOrder->getExecutionDay());
+
+ parent::__construct(
+ static::NAME,
+ $segmentNumber,
+ $version,
+ array(
+ $kti,
+ $SEPADescriptor,
+ new Bin($SEPAStandingOrder->getXML()),
+ "",
+ $SEPAStandingOrder->getId(),
+ $deg
+ )
+ );
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return static::NAME;
+ }
+}
diff --git a/lib/Fhp/Segment/HKDSC.php b/lib/Fhp/Segment/HKDSC.php
new file mode 100644
index 0000000..cb70712
--- /dev/null
+++ b/lib/Fhp/Segment/HKDSC.php
@@ -0,0 +1,51 @@
+
+ * @package Fhp\Segment
+ */
+class HKDSC extends AbstractSegment
+{
+ const NAME = 'HKDSC';
+ const VERSION = 1;
+
+ /**
+ * HKDSC constructor.
+ * @param int $version
+ * @param int $segmentNumber
+ * @param Kti $kti
+ * @param string $SEPADescriptor
+ * @param string $painMessage
+ */
+ public function __construct($version, $segmentNumber, $kti, $SEPADescriptor, $painMessage)
+ {
+ parent::__construct(
+ static::NAME,
+ $segmentNumber,
+ $version,
+ array(
+ $kti,
+ $SEPADescriptor,
+ new Bin($painMessage)
+ )
+ );
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return static::NAME;
+ }
+}
diff --git a/lib/Fhp/Segment/HKDSE.php b/lib/Fhp/Segment/HKDSE.php
new file mode 100644
index 0000000..b1b1f1d
--- /dev/null
+++ b/lib/Fhp/Segment/HKDSE.php
@@ -0,0 +1,51 @@
+
+ * @package Fhp\Segment
+ */
+class HKDSE extends AbstractSegment
+{
+ const NAME = 'HKDSE';
+ const VERSION = 1;
+
+ /**
+ * HKDSE constructor.
+ * @param int $version
+ * @param int $segmentNumber
+ * @param Kti $kti
+ * @param string $SEPADescriptor
+ * @param string $painMessage
+ */
+ public function __construct($version, $segmentNumber, $kti, $SEPADescriptor, $painMessage)
+ {
+ parent::__construct(
+ static::NAME,
+ $segmentNumber,
+ $version,
+ array(
+ $kti,
+ $SEPADescriptor,
+ new Bin($painMessage)
+ )
+ );
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return static::NAME;
+ }
+}
diff --git a/lib/Fhp/Segment/HKEND.php b/lib/Fhp/Segment/HKEND.php
index 231a2fe..0e478f4 100644
--- a/lib/Fhp/Segment/HKEND.php
+++ b/lib/Fhp/Segment/HKEND.php
@@ -2,6 +2,8 @@
namespace Fhp\Segment;
+use Fhp\FinTsInternal;
+
/**
* Class HKEND (Dialogende)
* Segment type: Administration
@@ -27,7 +29,7 @@ public function __construct($segmentNumber, $dialogId)
static::NAME,
$segmentNumber,
static::VERSION,
- array($dialogId)
+ array(FinTsInternal::escapeString($dialogId))
);
}
diff --git a/lib/Fhp/Segment/HKTAN.php b/lib/Fhp/Segment/HKTAN.php
new file mode 100644
index 0000000..a0d9342
--- /dev/null
+++ b/lib/Fhp/Segment/HKTAN.php
@@ -0,0 +1,77 @@
+
+ * @package Fhp\Segment
+ */
+class HKTAN extends AbstractSegment
+{
+ const NAME = 'HKTAN';
+ const VERSION = 6;
+
+ /**
+ * HKCDL constructor.
+ * @param int $version
+ * @param int $segmentNumber
+ */
+ public function __construct($version, $segmentNumber, $processID = null)
+ {
+ $data = array();
+ $data[] = 4;
+ if($processID){
+ $data = array();
+ if ($version == 6) {
+ $data[] = 2;
+ $data[] = "";
+ $data[] = "";
+ $data[] = "";
+ $data[] = $processID;
+ $data[] = "N";
+ } else {
+ $data[] = 2;
+ $data[] = "";
+ $data[] = "";
+ $data[] = "";
+ $data[] = $processID;
+ $data[] = "";
+ $data[] = "N";
+ }
+
+ #$data[] = 2;
+ #$data[] = "";
+ #$data[] = "";
+ #$data[] = "";
+ #$data[] = $processID;
+ } else {
+ $data[] = "HKIDN";
+ $data[] = "";
+ $data[] = "";
+ $data[] = "";
+ $data[] = "N";
+ }
+
+
+ parent::__construct(
+ static::NAME,
+ $segmentNumber,
+ $version,
+ $data
+ );
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return static::NAME;
+ }
+}
diff --git a/lib/Fhp/Segment/HNHBK.php b/lib/Fhp/Segment/HNHBK.php
index b226bd3..53f1b0b 100644
--- a/lib/Fhp/Segment/HNHBK.php
+++ b/lib/Fhp/Segment/HNHBK.php
@@ -2,6 +2,8 @@
namespace Fhp\Segment;
+use Fhp\FinTsInternal;
+
/**
* Class HNHBK (Nachrichtenkopf)
* Segment type: Administration
@@ -36,7 +38,7 @@ public function __construct($messageLength, $dialogId, $messageNumber)
array(
$messageLength,
300, // HBCI / FINTS version 3.0,
- $dialogId,
+ FinTsInternal::escapeString($dialogId),
$messageNumber,
)
);
diff --git a/lib/Fhp/Segment/HNSHA.php b/lib/Fhp/Segment/HNSHA.php
index 0b21d34..060e422 100644
--- a/lib/Fhp/Segment/HNSHA.php
+++ b/lib/Fhp/Segment/HNSHA.php
@@ -3,7 +3,7 @@
namespace Fhp\Segment;
use Fhp\DataTypes\Kik;
-
+use Fhp\Deg;
/**
* Class HNSHA (Signaturabschluss)
* Segment type: Administration
@@ -27,8 +27,14 @@ class HNSHA extends AbstractSegment
public function __construct(
$segmentNumber,
$securityControlReference,
- $pin
+ $pin,
+ $tan = null
) {
+ $deg = new Deg();
+ $deg->addDataElement($pin);
+ if($tan)
+ $deg->addDataElement ($tan);
+
parent::__construct(
static::NAME,
$segmentNumber,
@@ -36,7 +42,7 @@ public function __construct(
array(
$securityControlReference,
'',
- $pin
+ $deg
)
);
}
diff --git a/lib/Fhp/Segment/SegmentDescriptor.php b/lib/Fhp/Segment/SegmentDescriptor.php
new file mode 100644
index 0000000..5f68bf1
--- /dev/null
+++ b/lib/Fhp/Segment/SegmentDescriptor.php
@@ -0,0 +1,73 @@
+class = $class;
+ try {
+ $clazz = new \ReflectionClass($class);
+ if (!$clazz->isSubclassOf(BaseSegment::class)) {
+ throw new \InvalidArgumentException("Must inherit from BaseSegment: $class");
+ }
+ parent::__construct($clazz);
+
+ // Parse the class name into segment type (Kennung) and version.
+ if (preg_match('/^([A-Z]+)v([0-9]+)$/', $clazz->getShortName(), $match) !== 1) {
+ throw new \InvalidArgumentException("Invalid segment class name: $class");
+ }
+ $this->kennung = $match[1];
+ $this->version = intval($match[2]);
+ } catch (\ReflectionException $e) {
+ throw new \RuntimeException($e);
+ }
+ }
+
+ public function validateObject($obj) // Override
+ {
+ parent::validateObject($obj);
+ if (!($obj instanceof BaseSegment)) {
+ throw new \InvalidArgumentException("Expected sub-class of BaseSegment, got " . gettype($obj));
+ }
+ if ($obj->getName() !== $this->kennung) {
+ throw new \InvalidArgumentException("Expected $this->kennung, got " . $obj->getName());
+ }
+ DegDescriptor::get(Segmentkopf::class)->validateObject($obj->segmentkopf);
+
+ }
+}
diff --git a/lib/Fhp/Segment/Segmentkopf.php b/lib/Fhp/Segment/Segmentkopf.php
new file mode 100644
index 0000000..4d28486
--- /dev/null
+++ b/lib/Fhp/Segment/Segmentkopf.php
@@ -0,0 +1,35 @@
+elements as $index => $elementDescriptor) {
+ $offset += ($index - $expectedIndex); // Adjust for skipped indices.
+ $numRepetitions = $elementDescriptor->repeated === 0 ? 1 : $elementDescriptor->repeated;
+ $expectedIndex += $numRepetitions; // Advance to next expected elementDescriptor index.
+
+ // Skip optional elements that are not present.
+ if (!isset($rawElements[$offset]) || $rawElements[$offset] === '') {
+ if ($elementDescriptor->optional) {
+ $offset++;
+ continue;
+ }
+ throw new \InvalidArgumentException("Missing field $elementDescriptor->field");
+ }
+
+ // Parse element (possibly multiple values recursively).
+ try {
+ for ($repetition = 0; $repetition < $numRepetitions; $repetition++) {
+ if ($offset >= count($rawElements)) {
+ break; // End of input reached
+ }
+ if (is_string($elementDescriptor->type)) { // Scalar type / DE
+ if ($rawElements[$offset] === '' && $repetition >= 1) { // Skip empty repeated entries.
+ $offset++;
+ continue;
+ }
+ $value = static::parseDataElement($rawElements[$offset], $elementDescriptor->type);
+ $offset++;
+ } else { // Nested DEG, will consume a certain number of elements and adjust the $offset accordingly.
+ list($value, $offset) =
+ static::parseDegElements($rawElements, $elementDescriptor->type->name, $offset);
+ }
+ if ($elementDescriptor->repeated === 0) {
+ $result->{$elementDescriptor->field} = $value;
+ } else {
+ $result->{$elementDescriptor->field}[] = $value;
+ }
+ }
+ } catch (\InvalidArgumentException $e) {
+ throw new \InvalidArgumentException("Failed to parse $descriptor->class::$elementDescriptor->field: $e");
+ }
+ }
+ return array($result, $offset);
+ }
+
+ /**
+ * @param string $rawSegment The serialized wire format for a single segment (segment delimiter may be present at
+ * the end, or not).
+ * @param string $type The type (PHP class name) of the segment to be parsed.
+ * @return BaseSegment The parsed segment of type $type.
+ */
+ public static function parseSegment($rawSegment, $type)
+ {
+ $descriptor = SegmentDescriptor::get($type);
+ if (substr($rawSegment, -1) === Delimiter::SEGMENT) {
+ $rawSegment = substr($rawSegment, 0, -1); // Strip segment delimiter at the end, if present.
+ }
+ $rawElements = static::splitEscapedString(Delimiter::ELEMENT, $rawSegment);
+ if (empty($rawElements)) {
+ throw new \InvalidArgumentException("Invalid segment: $rawSegment");
+ }
+
+ /** @var Segmentkopf $segmentkopf */
+ $segmentkopf = static::parseDeg($rawElements[0], Segmentkopf::class);
+ if ($segmentkopf->segmentkennung !== $descriptor->kennung) {
+ throw new \InvalidArgumentException("Invalid segment type $segmentkopf->segmentkennung for $type");
+ }
+ if ($segmentkopf->segmentversion !== $descriptor->version) {
+ throw new \InvalidArgumentException("Invalid version $segmentkopf->segmentversion for $type");
+ }
+
+ $result = new $type();
+ $result->segmentkopf = $segmentkopf;
+ // The iteration order guarantees that $index is strictly monotonically increasing, but there can be gaps.
+ foreach ($descriptor->elements as $index => $elementDescriptor) {
+ if (!isset($rawElements[$index]) || $rawElements[$index] === '') {
+ if ($elementDescriptor->optional) {
+ continue;
+ }
+ throw new \InvalidArgumentException("Missing field $elementDescriptor->field");
+ }
+
+ if ($elementDescriptor->repeated === 0) {
+ $result->{$elementDescriptor->field} =
+ static::parseSegmentElement($rawElements[$index], $elementDescriptor);
+ } else {
+ for ($repetition = 0; $repetition < $elementDescriptor->repeated; $repetition++) {
+ if ($index + $repetition >= count($rawElements)) {
+ break; // End of input reached.
+ }
+ if ($rawElements[$index + $repetition] !== '') { // Skip empty entries.
+ $result->{$elementDescriptor->field}[$repetition] =
+ static::parseSegmentElement($rawElements[$index + $repetition], $elementDescriptor);
+ }
+ }
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * @param string $rawElement The raw content (unparsed wire format) of an element, which can either be a single
+ * Data Element (DE) or a group (DEG), as determined by the descriptor.
+ * @param ElementDescriptor $descriptor The descriptor that describes the expected format of the element.
+ * @return mixed The parsed value.
+ */
+ private static function parseSegmentElement($rawElement, $descriptor)
+ {
+ return is_string($descriptor->type)
+ ? static::parseDataElement($rawElement, $descriptor->type)
+ : static::parseDeg($rawElement, $descriptor->type->name);
+ }
+}
diff --git a/lib/Fhp/Syntax/Serializer.php b/lib/Fhp/Syntax/Serializer.php
new file mode 100644
index 0000000..fe43ea1
--- /dev/null
+++ b/lib/Fhp/Syntax/Serializer.php
@@ -0,0 +1,112 @@
+segmentkopf->serialize();
+ return implode(Delimiter::ELEMENT, $serializedElements) . Delimiter::SEGMENT;
+ }
+
+ /**
+ * @param BaseSegment|BaseDeg $obj An object to be serialized.
+ * @return string[] A partial serialization of that object, namely an array with all of its elements serialized
+ * independently, and at the right indices (i.e. the returned array may contain empty strings as gaps/buffers).
+ */
+ private static function serializeElements($obj)
+ {
+ $serializedElements = array();
+ foreach ($obj->getDescriptor()->elements as $index => $elementDescriptor) {
+ $value = $obj->{$elementDescriptor->field};
+ if ($value === null) continue;
+ if (isset($serializedElements[$index])) throw new \AssertionError();
+ if ($elementDescriptor->repeated === 0) {
+ $serializedElements[$index] = static::serializeElement($value, $elementDescriptor->type);
+ } else {
+ foreach ($value as $offset => $item) {
+ $serializedElements[$index + $offset] = static::serializeElement($item, $elementDescriptor->type);
+ }
+ }
+ }
+ static::fillMissingKeys($serializedElements, '');
+ return $serializedElements;
+ }
+
+ private static function serializeElement($value, $type)
+ {
+ return is_string($type) ? static::serializeDataElement($value, $type) : static::serializeDeg($value);
+ }
+
+ /**
+ * Public for testing only.
+ * @param array $arr An array with numeric sorted keys, e.g. `[0 => 'a', 2 => 'b', 4 => 'c']`. After this function
+ * returns, all missing keys (gaps) will be filled in with the $value, e.g. if `$value == 'X'` the result would
+ * be `[0 => 'a', 1 => 'X', 2 => 'b', 3 => 'X', 4 => 'c'].
+ * @param mixed $value The value to fill in.
+ */
+ public static function fillMissingKeys(&$arr, $value)
+ {
+ if (empty($arr)) return;
+ $lastKey = array_key_last($arr);
+ if (!is_numeric($lastKey)) throw new \InvalidArgumentException("Keys must be numeric, got $lastKey");
+ for ($key = 0; $key < $lastKey; $key++) {
+ if (!array_key_exists($key, $arr)) {
+ $arr[$key] = $value;
+ }
+ }
+ ksort($arr);
+ }
+}
diff --git a/lib/Tests/Fhp/ConnectionTest.php b/lib/Tests/Fhp/ConnectionTest.php
deleted file mode 100644
index cf1c43d..0000000
--- a/lib/Tests/Fhp/ConnectionTest.php
+++ /dev/null
@@ -1,47 +0,0 @@
-adapter = $this->getMockBuilder('\Fhp\Adapter\Curl')
- ->disableOriginalConstructor()
- ->setMethods(array('send'))
- ->getMock();
-
- $this->message = $this->getMockBuilder('\Fhp\Message\Message')
- ->disableOriginalConstructor()
- ->getMock();
- }
-
- public function test_can_set_and_get_adapter()
- {
- $conn = new Connection($this->adapter);
- $this->assertEquals($this->adapter, $conn->getAdapter());
- }
-
- public function test_send_calls_adapter_send()
- {
- $this->adapter->expects($this->once())
- ->method('send')
- ->with($this->message)
- ->will($this->returnValue('response text'));
-
- $conn = new Connection($this->adapter);
- $res = $conn->send($this->message);
-
- $this->assertInternalType('string', $res);
- }
-}
diff --git a/lib/Tests/Fhp/Model/StatementOfAccount/TransactionTest.php b/lib/Tests/Fhp/Model/StatementOfAccount/TransactionTest.php
index 9a44664..e28fbe9 100644
--- a/lib/Tests/Fhp/Model/StatementOfAccount/TransactionTest.php
+++ b/lib/Tests/Fhp/Model/StatementOfAccount/TransactionTest.php
@@ -34,4 +34,13 @@ public function test_getter_and_setter()
$this->assertSame('desc2', $obj->setDescription2('desc2')->getDescription2());
$this->assertSame('name', $obj->setName('name')->getName());
}
+
+ public function test_get_signed_amount()
+ {
+ $obj = new Transaction();
+ $this->assertSame(-20.00, $obj->setCreditDebit(Transaction::CD_DEBIT)->setAmount(20.00)->getSignedAmount());
+ $this->assertSame(-20.00, $obj->setCreditDebit(Transaction::CD_CREDIT_CANCELLATION)->setAmount(20.00)->getSignedAmount());
+ $this->assertSame(20.00, $obj->setCreditDebit(Transaction::CD_CREDIT)->setAmount(20.00)->getSignedAmount());
+ $this->assertSame(20.00, $obj->setCreditDebit(Transaction::CD_DEBIT_CANCELLATION)->setAmount(20.00)->getSignedAmount());
+ }
}
diff --git a/lib/Tests/Fhp/Parser/MT940Test.php b/lib/Tests/Fhp/Parser/MT940Test.php
index 5dbeb89..b5e1d86 100644
--- a/lib/Tests/Fhp/Parser/MT940Test.php
+++ b/lib/Tests/Fhp/Parser/MT940Test.php
@@ -6,29 +6,210 @@
class MT940Test extends \PHPUnit_Framework_TestCase
{
+ public function testYearChange()
+ {
+ function createTestData($vd, $bd) {
+ return <<parse(MT940::TARGET_ARRAY)[0]['transactions'][0];
+ $this->assertEquals('2019-01-01', $tx['valuta_date']);
+ $this->assertEquals('2018-12-31', $tx['booking_date']);
+
+ $parser = new MT940(preg_replace('/\r?\n/', "\r\n", createTestData('181231', '0101')));
+ $tx = $parser->parse(MT940::TARGET_ARRAY)[0]['transactions'][0];
+ $this->assertEquals('2019-01-01', $tx['booking_date']);
+ $this->assertEquals('2018-12-31', $tx['valuta_date']);
+
+ $parser = new MT940(preg_replace('/\r?\n/', "\r\n", createTestData('181231', '1231')));
+ $tx = $parser->parse(MT940::TARGET_ARRAY)[0]['transactions'][0];
+ $this->assertEquals('2018-12-31', $tx['booking_date']);
+ $this->assertEquals('2018-12-31', $tx['valuta_date']);
+
+ $parser = new MT940(preg_replace('/\r?\n/', "\r\n", createTestData('190101', '0101')));
+ $tx = $parser->parse(MT940::TARGET_ARRAY)[0]['transactions'][0];
+ $this->assertEquals('2019-01-01', $tx['booking_date']);
+ $this->assertEquals('2019-01-01', $tx['valuta_date']);
+ }
+
+
+ public function testEarmarkedTransactionsSparkasse()
+ {
+ $data = <<parse(MT940::TARGET_ARRAY);
+ $this->assertEquals(1, sizeof($result));
+ foreach ($result as $stmt) {
+ $this->assertNotNull($stmt['date']);
+ $this->assertEquals(':61:1803050305CR1337,00N062NONREF', $stmt['transactions']['0']['turnover_raw']);
+ $this->assertEquals(':86:999PN5477SCHECK-NR. 0000016703074', $stmt['transactions']['0']['multi_purpose_raw']);
+ }
+ }
+
+ public function testParseAtAt()
+ {
+ // VBKM sent this as single line, \n is just for better readability
+ $atat = preg_replace(
+ '/\n/',
+ '',
+ <<parse(MT940::TARGET_ARRAY);
+ $this->assertEquals(4, sizeof($result));
+ $this->assertEquals(161000.01, $result[2]['start_balance']['amount']);
+ $this->assertEquals(
+ 'Darl. Abrechnung 1000000000Akt. Sollzinssatz 1,4000%',
+ $result[3]['transactions'][0]['description']['description_1']
+ );
+ }
+
/**
* Parser must be able to parse description lines starting with "-".
*/
public function testParseDescriptionLines()
{
$rawData = [
- ":20:STARTUMSE",
- ":25:00000000/0221122370",
- ":28C:00000/001",
- ":60F:C170428EUR6670,54",
- ":61:1705030503CR223,72N062NONREF",
- ":86:166?00GUTSCHRIFT?109251?20EREF+CCB.122.UE.266455?21SVWZ+Re 17",
- "-H-0005 vom 24.04?22.2017?30DRESDEFF850?31DE00000000000000000000?",
- "32TEST TEST GBR",
- ":62F:C170503EUR6894,26",
- "-"
+ ':20:STARTUMSE',
+ ':25:00000000/0221122370',
+ ':28C:00000/001',
+ ':60F:C170428EUR6670,54',
+ ':61:1705030503CR223,72N062NONREF',
+ ':86:166?00GUTSCHRIFT?109251?20EREF+CCB.122.UE.266455?21SVWZ+Re 17',
+ '-H-0005 vom 24.04?22.2017?30DRESDEFF850?31DE00000000000000000000?',
+ '32TEST TEST GBR',
+ ':62F:C170503EUR6894,26',
+ '-',
+ ':20:STARTUMSE',
+ ':25:00000000/0221122370',
+ ':28C:00000/001',
+ ':60M:C170428EUR6894,26',
+ ':61:1705030503CR3105.74N062NONREF',
+ ':86:166?00GUTSCHRIFT?109251?20EREF+CCB.122.UE.266455?21SVWZ+Re 17',
+ '-H-0005 vom 24.04?22.2017?30DRESDEFF850?31DE00000000000000000000?',
+ '32TEST TEST GBR',
+ ':62F:C170503EUR10000,00',
+ '-',
+ ':20:STARTUMSE',
+ ':25:00000000/0221122370',
+ ':28C:00000/001',
+ ':60F:C170429EUR10000,00',
+ ':61:1705030503CR100,00N062NONREF',
+ ':86:166?00GUTSCHRIFT?109251?20EREF+CCB.122.UE.266455?21SVWZ+Re 17',
+ '-H-0005 vom 24.04?22.2017?30DRESDEFF850?31DE00000000000000000000?',
+ '32TEST TEST GBR',
+ ':62F:C170503EUR10100,00',
+ '-'
];
$parser = new MT940(implode("\r\n", $rawData));
$result = $parser->parse(MT940::TARGET_ARRAY);
$this->assertEquals(
'EREF+CCB.122.UE.266455SVWZ+Re 17-H-0005 vom 24.04.2017',
- $result['2017-04-28']['transactions'][0]['description']['description_1']
+ $result[0]['transactions'][0]['description']['description_1']
+ );
+ $this->assertEquals(
+ '166',
+ $result[0]['transactions'][0]['transaction_code']
+ );
+ $this->assertEquals(
+ '223.72',
+ $result[0]['transactions'][0]['amount']
+ );
+ $this->assertEquals(
+ '2017-04-29',
+ $result[2]['date']
+ );
+ $this->assertEquals(
+ 10000.00,
+ $result[2]['start_balance']['amount']
);
}
}
diff --git a/lib/Tests/Fhp/ResponseTest/GetVariablesTest.php b/lib/Tests/Fhp/ResponseTest/GetVariablesTest.php
new file mode 100644
index 0000000..2c5d058
--- /dev/null
+++ b/lib/Tests/Fhp/ResponseTest/GetVariablesTest.php
@@ -0,0 +1,43 @@
+parseTanModes($segments);
+ $this->assertEquals($modes[910], 'chipTAN manuell');
+ $this->assertEquals($modes[911], 'chipTAN optisch');
+ $this->assertEquals($modes[912], 'chipTAN-USB');
+ $this->assertEquals($modes[913], 'chipTAN-QR');
+ $this->assertEquals($modes[920], 'smsTAN');
+ $this->assertEquals($modes[921], 'pushTAN');
+ $this->assertEquals($modes[900], 'iTAN');
+ }
+
+ public function testParseTanModesPostbank()
+ {
+ // postbank provides different versions - maybe we just use the highest one?
+ $segments = array(
+ 'HITANS:14:4:4+1+1+0+N:N:0:901:2:mobileTAN:mobileTAN::mobileTAN:6:2:mobileTAN:999:0:N:1:0:N:N:N:N:J:00:2:9:910:2:HHD1.3.2OPT:HHDOPT1:1.3.2:chipTAN optisch HHD1.3.2:6:1:Challenge:999:0:N:1:0:N:N:N:N:J:00:2:9:911:2:HHD1.3.2:HHD:1.3.2:chipTAN manuell HHD1.3.2:6:1:Challenge:999:0:N:1:0:N:N:N:N:J:00:2:9:920:2:BestSign:BestSign::BestSign:6:2:BestSign:999:0:N:1:0:N:N:N:N:J:00:2:9:930:2:mobileTAN:mobileTAN::mobileTAN:6:2:mobileTAN:999:0:N:1:0:N:N:N:N:J:00:2:9',
+ 'HITANS:15:5:4+1+1+0+N:N:0:901:2:mobileTAN:mobileTAN::mobileTAN:6:2:mobileTAN:999:0:N:1:0:N:0:2:N:J:00:2:9:910:2:HHD1.3.2OPT:HHDOPT1:1.3.2:chipTAN optisch HHD1.3.2:6:1:Challenge:999:0:N:1:0:N:0:2:N:J:00:2:9:911:2:HHD1.3.2:HHD:1.3.2:chipTAN manuell HHD1.3.2:6:1:Challenge:999:0:N:1:0:N:0:2:N:J:00:2:9:912:2:HHD1.4OPT:HHDOPT1:1.4:chipTAN optisch HHD1.4:6:1:Challenge:999:0:N:1:0:N:0:2:N:J:00:2:9:913:2:HHD1.4:HHD:1.4:chipTAN manuell HHD1.4:6:1:Challenge:999:0:N:1:0:N:0:2:N:J:00:2:9:920:2:BestSign:BestSign::BestSign:6:2:BestSign:999:0:N:1:0:N:0:2:N:J:00:2:9:930:2:mobileTAN:mobileTAN::mobileTAN:6:2:mobileTAN:999:0:N:1:0:N:0:2:N:J:00:2:9',
+ 'HITANS:16:6:4+1+1+0+N:N:0:901:2:mobileTAN:mobileTAN::mobileTAN:6:2:mobileTAN:999:N:1:N:0:2:N:J:00:2:N:9:910:2:HHD1.3.2OPT:HHDOPT1:1.3.2:chipTAN optisch HHD1.3.2:6:1:Challenge:999:N:1:N:0:2:N:J:00:2:N:9:911:2:HHD1.3.2:HHD:1.3.2:chipTAN manuell HHD1.3.2:6:1:Challenge:999:N:1:N:0:2:N:J:00:2:N:9:912:2:HHD1.4OPT:HHDOPT1:1.4:chipTAN optisch HHD1.4:6:1:Challenge:999:N:1:N:0:2:N:J:00:2:N:9:913:2:HHD1.4:HHD:1.4:chipTAN manuell HHD1.4:6:1:Challenge:999:N:1:N:0:2:N:J:00:2:N:9:920:2:BestSign:BestSign::BestSign:6:2:BestSign:999:N:1:N:0:2:N:J:00:2:N:9:930:2:mobileTAN:mobileTAN::mobileTAN:6:2:mobileTAN:999:N:1:N:0:2:N:J:00:2:N:9'
+ );
+ $gv = new GetVariables(null, null);
+ $modes = $gv->parseTanModes($segments);
+ $this->assertEquals($modes[901], "mobileTAN");
+ $this->assertEquals($modes[910], "chipTAN optisch HHD1.3.2");
+ $this->assertEquals($modes[911], "chipTAN manuell HHD1.3.2");
+ $this->assertEquals($modes[920], "BestSign");
+ $this->assertEquals($modes[930], "mobileTAN");
+ $this->assertEquals($modes[912], "chipTAN optisch HHD1.4");
+ $this->assertEquals($modes[913], "chipTAN manuell HHD1.4");
+ }
+}
diff --git a/lib/Tests/Fhp/Segment/HITANSTest.php b/lib/Tests/Fhp/Segment/HITANSTest.php
new file mode 100644
index 0000000..1c42314
--- /dev/null
+++ b/lib/Tests/Fhp/Segment/HITANSTest.php
@@ -0,0 +1,93 @@
+assertEquals(1, $parsed->maximaleAnzahlAuftraege);
+ $this->assertEquals(1, $parsed->anzahlSignaturenMindestens);
+ $this->assertEquals(1, $parsed->sicherheitsklasse);
+
+ $parsedParams = $parsed->parameterZweiSchrittTanEinreichung;
+ $this->assertEquals(true, $parsedParams->einschrittVerfahrenErlaubt);
+ $this->assertEquals(false, $parsedParams->mehrAlsEinTanPflichtigerAuftragProNachrichtErlaubt);
+ $this->assertEquals(0, $parsedParams->auftragsHashwertverfahren);
+ $this->assertEquals(0, $parsedParams->sicherheitsprofilBankenSignatureBeiHitan);
+ $this->assertCount(2, $parsedParams->verfahrensparameterZweiSchrittVerfahren);
+
+ $verfahren1 = $parsedParams->verfahrensparameterZweiSchrittVerfahren[0];
+ $this->assertEquals(920, $verfahren1->sicherheitsfunktion);
+ $this->assertEquals("smsTAN", $verfahren1->technischeIdentifikationTanVerfahren);
+ $this->assertEquals("TAN-Nummer", $verfahren1->textZurBelegungDesRueckgabewertes);
+ $this->assertEquals(true, $verfahren1->mehrfachTanErlaubt);
+
+ $verfahren1 = $parsedParams->verfahrensparameterZweiSchrittVerfahren[1];
+ $this->assertEquals(900, $verfahren1->sicherheitsfunktion);
+ $this->assertEquals("iTAN", $verfahren1->technischeIdentifikationTanVerfahren);
+ $this->assertEquals("TAN-Nummer", $verfahren1->textZurBelegungDesRueckgabewertes);
+ $this->assertEquals(true, $verfahren1->mehrfachTanErlaubt);
+ }
+
+ public function test_parse_DKB_response_v3_with_wrong_parser()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ HITANSv1::parse(static::REAL_DKB_RESPONSE[1]);
+ }
+
+ public function test_validate_DKB_response_v1()
+ {
+ /** @var HITANSv1 $parsed */
+ $parsed = HITANSv1::parse(static::REAL_DKB_RESPONSE[0]);
+ $parsed->validate(); // Should not throw.
+ }
+
+ public function test_serialize_DKB_response_v1()
+ {
+ $parsed = HITANSv1::parse(static::REAL_DKB_RESPONSE[0]);
+ $this->assertEquals(static::REAL_DKB_RESPONSE[0], $parsed->serialize());
+ }
+
+ public function test_parse_DKB_response_v3()
+ {
+ /** @var HITANSv3 $parsed */
+ $parsed = HITANSv3::parse(static::REAL_DKB_RESPONSE[1]);
+ $this->assertEquals(1, $parsed->maximaleAnzahlAuftraege);
+ $parsedParams = $parsed->parameterZweiSchrittTanEinreichung;
+ $this->assertEquals(true, $parsedParams->einschrittVerfahrenErlaubt);
+ $this->assertCount(7, $parsedParams->verfahrensparameterZweiSchrittVerfahren);
+ $this->assertEquals("HHD1.3.0", $parsedParams->verfahrensparameterZweiSchrittVerfahren[0]->technischeIdentifikationTanVerfahren);
+ $this->assertEquals("chipTAN manuell", $parsedParams->verfahrensparameterZweiSchrittVerfahren[0]->nameDesZweiSchrittVerfahrens);
+ $this->assertEquals("TAN2go", $parsedParams->verfahrensparameterZweiSchrittVerfahren[5]->technischeIdentifikationTanVerfahren);
+ $this->assertEquals("iTAN", $parsedParams->verfahrensparameterZweiSchrittVerfahren[6]->technischeIdentifikationTanVerfahren);
+ $this->assertEquals("00", $parsedParams->verfahrensparameterZweiSchrittVerfahren[6]->initialisierungsmodus);
+ }
+
+ public function test_validate_DKB_response_v3()
+ {
+ $parsed = HITANSv3::parse(static::REAL_DKB_RESPONSE[1]);
+ $parsed->validate(); // Should not throw.
+ }
+
+ public function test_serialize_DKB_response_v3()
+ {
+ $parsed = HITANSv3::parse(static::REAL_DKB_RESPONSE[1]);
+ $this->assertEquals(static::REAL_DKB_RESPONSE[1], $parsed->serialize());
+ }
+}
diff --git a/lib/Tests/Fhp/Segment/HIUPDTest.php b/lib/Tests/Fhp/Segment/HIUPDTest.php
new file mode 100644
index 0000000..0e43d14
--- /dev/null
+++ b/lib/Tests/Fhp/Segment/HIUPDTest.php
@@ -0,0 +1,87 @@
+assertSame(16, $parsed->segmentkopf->segmentnummer);
+ $this->assertSame(4, $parsed->segmentkopf->segmentversion);
+ $this->assertSame('1234567', $parsed->kontoverbindung->kontonummer);
+ $this->assertNull($parsed->kontoverbindung->unterkontomerkmal);
+ $this->assertSame(280, $parsed->kontoverbindung->laenderkennzeichen);
+ $this->assertSame('10020030', $parsed->kontoverbindung->kreditinstitutionscode);
+ $this->assertSame('12345', $parsed->kundenId);
+ $this->assertSame('DEM', $parsed->kontowaehrung);
+ $this->assertSame('Ernst Müller', $parsed->name1);
+ $this->assertSame('Giro Spezial', $parsed->kontoproduktbezeichnung);
+
+ $this->assertSame('T', $parsed->kontolimit->limitart);
+ $this->assertSame(2000.0, $parsed->kontolimit->limitbetrag->wert);
+ $this->assertSame('DEM', $parsed->kontolimit->limitbetrag->waehrung);
+ $this->assertNull($parsed->kontolimit->limitTage);
+
+ $this->assertCount(9, $parsed->erlaubteGeschaeftsvorfaelle);
+ }
+
+ public function test_validate_HBCI22_example1()
+ {
+ /** @var HIUPDv4 $parsed */
+ $parsed = HIUPDv4::parse(static::HBCI22_EXAMPLES[0]);
+ $parsed->validate(); // Should not throw.
+ }
+
+ public function test_serialize_HBCI22_example1()
+ {
+ /** @var HIUPDv4 $parsed */
+ $parsed = HIUPDv4::parse(static::HBCI22_EXAMPLES[0]);
+ $this->assertEquals(static::HBCI22_EXAMPLES[0], $parsed->serialize());
+ }
+
+ public function test_parse_HBCI22_example2()
+ {
+ /** @var HIUPDv4 $parsed */
+ $parsed = HIUPDv4::parse(static::HBCI22_EXAMPLES[1]);
+ $this->assertSame('1234568', $parsed->kontoverbindung->kontonummer);
+ $this->assertSame('Sparkonto 2000', $parsed->kontoproduktbezeichnung);
+ $this->assertNull($parsed->kontolimit);
+
+ $this->assertCount(8, $parsed->erlaubteGeschaeftsvorfaelle);
+ $this->assertSame('HKUEB', $parsed->erlaubteGeschaeftsvorfaelle[4]->geschaeftsvorfall);
+ $this->assertSame(2, $parsed->erlaubteGeschaeftsvorfaelle[4]->anzahlBenoetigterSignaturen);
+ $this->assertSame('Z', $parsed->erlaubteGeschaeftsvorfaelle[4]->limitart);
+ $this->assertSame(1000.0, $parsed->erlaubteGeschaeftsvorfaelle[4]->limitbetrag->wert);
+ $this->assertSame('DEM', $parsed->erlaubteGeschaeftsvorfaelle[4]->limitbetrag->waehrung);
+ $this->assertSame(7, $parsed->erlaubteGeschaeftsvorfaelle[4]->limitTage);
+ }
+
+ public function test_validate_HBCI22_example2()
+ {
+ $parsed = HIUPDv4::parse(static::HBCI22_EXAMPLES[1]);
+ $parsed->validate(); // Should not throw.
+ }
+
+ public function test_serialize_HBCI22_example2()
+ {
+ $parsed = HIUPDv4::parse(static::HBCI22_EXAMPLES[1]);
+ $this->assertEquals(static::HBCI22_EXAMPLES[1], $parsed->serialize());
+ }
+}
diff --git a/lib/Tests/Fhp/Syntax/ParserTest.php b/lib/Tests/Fhp/Syntax/ParserTest.php
new file mode 100644
index 0000000..818d743
--- /dev/null
+++ b/lib/Tests/Fhp/Syntax/ParserTest.php
@@ -0,0 +1,78 @@
+assertEquals(array(), Parser::splitEscapedString('+', ''));
+ $this->assertEquals(array('', ''), Parser::splitEscapedString('+', '+'));
+ }
+
+ public function test_splitEscapedString_without_escaping()
+ {
+ $this->assertEquals(array('ABC', 'DEF'), Parser::splitEscapedString('+', 'ABC+DEF'));
+ $this->assertEquals(array('ABC', '', 'DEF'), Parser::splitEscapedString('+', 'ABC++DEF'));
+ $this->assertEquals(array('ABC', ''), Parser::splitEscapedString('+', 'ABC+'));
+ $this->assertEquals(array('', '', 'ABC'), Parser::splitEscapedString('+', '++ABC'));
+ }
+
+ public function test_splitEscapedString_with_escaping()
+ {
+
+ $this->assertEquals(array('A?+', 'DEF'), Parser::splitEscapedString('+', 'A?++DEF'));
+ $this->assertEquals(array('?+C', '', 'D?+'), Parser::splitEscapedString('+', '?+C++D?+'));
+ $this->assertEquals(array('ABC', '?+'), Parser::splitEscapedString('+', 'ABC+?+'));
+ $this->assertEquals(array('', '', '?+C'), Parser::splitEscapedString('+', '++?+C'));
+ }
+
+ public function test_unescape()
+ {
+ $this->assertEquals('ABC+DEF', Parser::unescape('ABC+DEF'));
+ $this->assertEquals('ABC+DEF', Parser::unescape('ABC?+DEF'));
+ $this->assertEquals('ABC?+DEF', Parser::unescape('ABC??+DEF'));
+ $this->assertEquals('ABC?DEF', Parser::unescape('ABC?DEF'));
+ $this->assertEquals('ABC:DEF', Parser::unescape('ABC?:DEF'));
+ }
+
+ public function test_parseDataElement()
+ {
+ $this->assertSame(15, Parser::parseDataElement('15', 'int'));
+ $this->assertSame(1000, Parser::parseDataElement('1000', 'integer'));
+ $this->assertSame(15.0, Parser::parseDataElement('15,', 'float'));
+ $this->assertSame(15.5, Parser::parseDataElement('15,5', 'float'));
+ $this->assertSame(0.0, Parser::parseDataElement('0,', 'float'));
+ $this->assertSame(true, Parser::parseDataElement('J', 'bool'));
+ $this->assertSame(false, Parser::parseDataElement('N', 'boolean'));
+ $this->assertSame("1000", Parser::parseDataElement('1000', 'string'));
+ }
+
+ public function test_parseDataElement_invalid_int()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ Parser::parseDataElement('lala', 'int');
+ }
+
+ public function test_parseDataElement_invalid_float_wrong_decimal_separator()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ Parser::parseDataElement('15.5', 'float');
+ }
+
+ public function test_parseDataElement_invalid_float_multiple_decimal_separator()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ Parser::parseDataElement('15,5,5', 'float');
+ }
+
+ public function test_parseDataElement_invalid_float_no_decimal_separator()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ Parser::parseDataElement('15', 'float');
+ }
+
+ // NOTE: Test coverage of DEGs and Segments is provided by tests in Test\Fhp\Segment.
+}
diff --git a/lib/Tests/Fhp/Syntax/SerializerTest.php b/lib/Tests/Fhp/Syntax/SerializerTest.php
new file mode 100644
index 0000000..5257cf6
--- /dev/null
+++ b/lib/Tests/Fhp/Syntax/SerializerTest.php
@@ -0,0 +1,27 @@
+assertSame('15', Serializer::serializeDataElement(15, 'int'));
+ $this->assertSame('1000', Serializer::serializeDataElement(1000, 'integer'));
+ $this->assertSame('15,', Serializer::serializeDataElement(15.0, 'float'));
+ $this->assertSame('15,5', Serializer::serializeDataElement(15.5, 'float'));
+ $this->assertSame('0,', Serializer::serializeDataElement(0.0, 'float'));
+ $this->assertSame('J', Serializer::serializeDataElement(true, 'bool'));
+ $this->assertSame('N', Serializer::serializeDataElement(false, 'boolean'));
+ $this->assertSame('1000', Serializer::serializeDataElement("1000", 'string'));
+ }
+
+ public function test_fillMissingKeys()
+ {
+ $arr = array(0 => 'a', 2 => 'b', 4 => 'c');
+ Serializer::fillMissingKeys($arr, 'X');
+ $this->assertEquals(array(0 => 'a', 1 => 'X', 2 => 'b', 3 => 'X', 4 => 'c'), $arr);
+ }
+}