phpFinTS icon indicating copy to clipboard operation
phpFinTS copied to clipboard

Postbank - getStatementOfAccount throws error

Open CnczubehoerEu opened this issue 6 years ago • 18 comments

Hello!

I'm trying to get the statement of my Postbank account.

$accounts = $fints->getSEPAAccounts(); var_dump($accounts); looks as it shoulds.

` $oneAccount = $accounts[0]; $from = new \DateTime('2019-11-01'); $to = new \DateTime();

    $soa = $fints->getStatementOfAccount($oneAccount, $from, $to);`

throws "Request Failed: Verarbeitung zur Zeit nicht möglich, bitte später erneut versuchen (9010); Teilweise fehlerhaft." However, I'm getting a SMS from Postbank with a TAN.

Do you have any idea to solve this issue?

Thank you very much in advance for your help.

Best regards,

Tobias

CnczubehoerEu avatar Nov 09 '19 14:11 CnczubehoerEu

You should take a look at the whole communication / log. I would guess that either on Dialog::init or Dialog::sync Postbank wants you to enter a TAN.

This is currently a work in progress topic.

ampaze avatar Nov 12 '19 09:11 ampaze

Can you please try with the new implementation in the library (see this example code) and report back if it works?

Philipp91 avatar Jan 04 '20 14:01 Philipp91

Okay, I did the following:

use Fhp\FinTsOptions;

class FinTSService
{
    /**
     * @param string $tan
     * @return \Fhp\FinTsNew
     * @throws \Fhp\CurlException
     * @throws \Fhp\Protocol\ServerException
     */
    public function login(string $tan = "")
    {
        $options = new \Fhp\FinTsOptions();
        $options->url = 'https://hbci.postbank.de/banking/hbci.do'; // HBCI / FinTS Url can be found here: https://www.hbci-zka.de/institute/institut_auswahl.htm (use the PIN/TAN URL)
        $options->bankCode = 'xxx'; // Your bank code / Bankleitzahl
        $options->productName = 'xxx'; // The number you receive after registration / FinTS-Registrierungsnummer
        $options->productVersion = '1.0'; // Your own Software product version
        $credentials = \Fhp\Credentials::create('xxx', 'xxx'); // This is NOT the PIN of your bank card!
        $options->logger = new \Tests\Fhp\SanitizingCLILogger([$options, $credentials]);
        $fints = new \Fhp\FinTsNew($options, $credentials);
        $fints->selectTanMode(930, "mT:Smartphones");
        /**
         * This function is key to how FinTS works in times of PSD2 regulations. Most actions like wire transfers, getting
         * statements and even logging in can require a TAN, but won't always. Whether a TAN is required depends on the kind of
         * action, when it was last executed, other parameters like the amount (of a wire transfer) or time span (of a statement
         * request) and generally the security concept of the particular bank. The TAN requirements may or may not be consistent
         * with the TAN that the same bank requires for the same action in the web-based online banking interface. Also, banks
         * may change these requirements over time, so just because your particular bank does not need a TAN for login today
         * does not mean that it stays that way.
         *
         * The TAN can be provided it many different ways. Each application that uses the phpFinTS library has to implement
         * its own way of asking users for a TAN, depending on its user interfaces. The implementation does not have to be in a
         * function like this, it can be inlined with the calling code, or live elsewhere. The TAN can be obtained while the
         * same PHP script is still running (i.e. handleTan() is a blocking function that only returns once the TAN is known),
         * but it is also possible to interrupt the PHP execution entirely while asking for the TAN.
         */
// Log in.
        $login = $fints->login();
        if ($login->needsTan()) {
            $this->handleTan($login, $tan);
        }
// Usage:
// $fints = require_once 'login.php';
        return $fints;
    }

    /**
     * @param \Fhp\BaseAction $action
     * @param string $tan
     * @throws \Fhp\CurlException
     * @throws \Fhp\Protocol\ServerException
     */
    protected function handleTan(\Fhp\BaseAction $action, string $tan)
    {
        global $fints, $options, $credentials;
        // Find out what sort of TAN we need, tell the user about it.
        //$tanRequest = $action->getTanRequest();
        //echo 'The bank requested a TAN, asking: ' . $tanRequest->getChallenge() . "\n";
        //if ($tanRequest->getTanMediumName() !== null) {
        //  echo 'Please use this device: ' . $tanRequest->getTanMediumName() . "\n";
        // }
        // Optional: Instead of printing the above to the console, you can relay the information (challenge and TAN medium)
        // to the user in any other way (through your REST API, a push notification, ...). If waiting for the TAN requires
        // you to interrupt this PHP session and the TAN will arrive in a fresh (HTTP/REST/...) request, you can do so:
        if ($tan == "") {
            $persistedAction = serialize($action);
            $persistedFints = $fints->persist();
            // These are two strings (watch out, they are NOT necessarily UTF-8 encoded), which you can store anywhere.
            // This example code stores them in a text file, but you might write them to your database (use a BLOB, not a
            // CHAR/TEXT field to allow for arbitrary encoding) or in some other storage (possibly base64-encoded to make it
            // ASCII).
            file_put_contents(__DIR__ . 'state.txt', serialize([$fints->persist(), serialize($action)]));
        }
        else {
            $restoredState = file_get_contents(__DIR__ . 'state.txt');
            list($persistedInstance, $persistedAction) = unserialize($restoredState);
            $fints = new \Fhp\FinTsNew($options, $credentials, $persistedInstance);
            $action = unserialize($persistedAction);
            $fints->submitTan($action, $tan);
        }
    }
}

I hope I have understood the example correctly. However, I think I still have a bug here in my code, but I'm a bit blind right now. I'm getting the following error:

Unexpected TAN request Exception Code: 0 Exception Type: Fhp\Protocol\UnexpectedResponseException Thrown in File: Packages/Libraries/nemiah/php-fints/lib/Fhp/FinTsNew.php Line: 248

However, I get a SMS after each call. Any hint? Thank you! :)

CnczubehoerEu avatar Jan 06 '20 13:01 CnczubehoerEu

It seems like the bank asks for a TAN before it has provided the BPD/Kundensystem-ID, which it should do without TANs.

Do you have a stack trace for this? Can you provide the log output from the SanitizingCLILogger? Then we can see how many requests succeeded beforehand, i.e. if it already sent the BPD/Kundensystem-ID (and the library just failed to interpret/store it) or whether it is actually asking for a TAN on the very first request already.

Philipp91 avatar Jan 06 '20 22:01 Philipp91

Thanks a lot for your help. I sent you the logger result via mail. I'm not quite sure, if it contains any non-public information.

CnczubehoerEu avatar Jan 08 '20 14:01 CnczubehoerEu

How did you figure out the TAN medium name "mT:Smartphones"? Just asking because in #200, someone reported that getTanMedia() doesn't work with Postbank yet. Anyway it seems to be the correct value, given that the bank confirms it and also sends an SMS. And does Postbank still have smsTAN/mobileTAN? From what I recall, PSD2 explicitly disallows that, and the Postbank page does not mention it anymore.

Philipp91 avatar Jan 08 '20 20:01 Philipp91

Deutsche Bank still allows and even offers mobileTAN. It seems it's compliant with PSD2. iTAN was shut down recently due to PSD2

nemiah avatar Jan 08 '20 20:01 nemiah

To figure out the string "mT:Smartphones" was hard work. The string "Smartphones" comes from the login page of the Postbank. On the login page PB asks for the way to transfer the TAN (select-field). There is written: "Smartphones / 9999999xxxx" => 9... my phonenumber. The "mt" was guessing. In the past, I found a protocoll or something like that from a banking software. But I really can not remember exactly.

CnczubehoerEu avatar Jan 08 '20 20:01 CnczubehoerEu

Feel free to execute getTanMedia(930) with the proposed hack (commenting out $this->ensureSynchronized(); inside that function) to verify your guess in yet another way.

Commenting out $this->ensureSynchronized(); would also be my proposal to make login() work -- it's called inside that function too. It seems like Postbank violates the specification by not supporting weakly authenticated dialogs for HKSYN, probably because they tie synchronization and UPD fetch together (and the latter is user data, so it naturally requires strong authentication). This wouldn't be totally foolish on their part, as it saves an entire client-server roundtrip without sacrificing any functionality -- just specification compatibility. So please let me know if login() works when you comment out the synchronization there.

Philipp91 avatar Jan 08 '20 20:01 Philipp91

Okay, I did the following:

        $fints = new \Fhp\FinTsNew($options, $credentials);
        echo $fints->getTanMedia(930);
        $fints->selectTanMode(930, "mT:Smartphones");

And the following errors comes: "Server does not allow single-step TAN mode, but there are multiple to pick from"

In another step, I changed the "FinTSNew.php":

    public function login(): DialogInitialization
    {
        $this->requireTanMode();
        // $this->ensureSynchronized();
        $this->messageNumber = 1;
        $login = new DialogInitialization($this->options, $this->credentials, $this->getSelectedTanMode(),
            $this->selectedTanMedium, $this->kundensystemId);
        $this->execute($login);
        return $login;
    }

The error "Unexpected TAN request" comes again.

CnczubehoerEu avatar Jan 08 '20 21:01 CnczubehoerEu

Server does not allow single-step TAN mode, but there are multiple to pick from

When you're calling getTanMedia(), you also have to comment out ensureSynchronized() in there.

The error "Unexpected TAN request" comes again.

Ah right. Above the line where that error comes from (around line 247), there is if ($this->bpd === null || $this->kundensystemId === null) {. Please also comment out the $this->kundensystemId === null part of this, as the ID will only be set a few lines further down (in processActionResponse()).

Please record your attempts with the SanitizingCLILogger. Once you get a successful one, store it somewhere on disk, we can make an integration test out of it. That's useful because, once we get this hack working, I need to refactor the code to detect this kind of bank automatically and react accordingly, without commenting out any commands. And the integration test helps me develop this without actually having a Postbank account myself.

Philipp91 avatar Jan 08 '20 21:01 Philipp91

Now, I did both: getTanMedia() an commented out ensureSynchronized. The error is the same: "Server does not allow single-step TAN mode, but there are multiple to pick from"

Additional I commented out: "$this->kundensystemId === null" => The error comes: "Call to a member function persist() on null"

CnczubehoerEu avatar Jan 08 '20 21:01 CnczubehoerEu

The error is the same: "Server does not allow single-step TAN mode, but there are multiple to pick from"

Can you provide a stack trace for this error?

Additional I commented out: "$this->kundensystemId === null" => The error comes: "Call to a member function persist() on null"

That sounds okay wrt. what the library is doing. Now it looks like you plumbed things together the wrong way in your FinTSService. Firstly, you're missing global $fints, ... in the first function, that's why it remains null in the second. Secondly, if you got a TAN request and persisted everything to state.txt and got a TAN, you shouldn't call login() again (that would start a second login attempt and send you a second TAN, making the first one invalid), but rather resume from the $restoredState right away.

Philipp91 avatar Jan 08 '20 22:01 Philipp91

I believe we fixed this, see discussion in #229 and the commit linked above. Let's close this (I can't though) and reopen in case the problem re-occurs.

Philipp91 avatar Apr 10 '20 08:04 Philipp91

Dear Philipp,

thank you very much for your efforts. I'm very sorry for my late reply. Lots of other stuff to do.

I implemented your samples into my system, and it works great, until Postbank wants to have a TAN (Postbank does not want to have to enter a TAN every time). My class:

class FinTSService implements PaymentServiceInterface
{
    /**
     * @var array
     */
    protected $settings = [];

    /**
     * @var PaymentMethodRepository
     * @Flow\Inject
     */
    protected $paymentMethodRepository;

    /**
     * @param array $settings
     */
    public function injectSettings(array $settings) {
        $this->settings = $settings;
    }

    /**
     * @param \DateTime $startDate
     * @param \DateTime $endDate
     * @param string $tan
     * @return array
     * @throws \Fhp\CurlException
     * @throws \Fhp\Protocol\ServerException
     */
    public function getTransfers(\DateTime $startDate, \DateTime $endDate, string $tan = "") : array {
        global $fints, $url, $bankCode, $username, $pin, $productName, $productVersion;

        $paymentMethod = $this->paymentMethodRepository->findOneByName("Postbank"); 

        $url = $this->settings["fints"]["institutes"]["postbank"]["url"]; // HBCI / FinTS Url can be found here: https://www.hbci-zka.de/institute/institut_auswahl.htm (use the PIN/TAN URL)
        $bankCode = $this->settings["fints"]["institutes"]["postbank"]["bankCode"]; // Your bank code / Bankleitzahl
        $productName = $this->settings["fints"]["registrationNumber"]; // The number you receive after registration / FinTS-Registrierungsnummer
        $productVersion = '1.0'; // Your own Software product version
        $username = $this->settings["fints"]["institutes"]["postbank"]["userName"];
        $pin = $this->settings["fints"]["institutes"]["postbank"]["pin"]; // This is NOT the PIN of your bank card!
        $fints = new FinTsNew($url, $bankCode, $username, $pin, $productName, $productVersion);
        //$fints->setLogger(new \Tests\Fhp\CLILogger());

        $fints->selectTanMode(930, "mT:Smartphones");

        // Log in.
        $login = $fints->login();
        if ($login->needsTan()) {
            $result = $this->handleTan($login, $tan);
            if ($result === "persisted")
                return [];
        }

        // Just pick the first account, for demonstration purposes. You could also have the user choose, or have SEPAAccount
        // hard-coded and not call getSEPAAccounts() at all.
        $getSepaAccounts = GetSEPAAccounts::create();
        $fints->execute($getSepaAccounts);
        if ($getSepaAccounts->needsTan()) {
            $result = $this->handleTan($getSepaAccounts, $tan); // See login.php for the implementation.
            if ($result === "persisted")
                return [];
        }
        $oneAccount = $getSepaAccounts->getAccounts()[0];

        $getStatement = \Fhp\Action\GetStatementOfAccount::create($oneAccount, $startDate, $endDate);
        $fints->execute($getStatement);
        if ($getStatement->needsTan()) {
            $this->handleTan($getStatement, $tan); // See login.php for the implementation.
        }

        $transfers = [];

        $soa = $getStatement->getStatement();
        foreach ($soa->getStatements() as $statement) {
            foreach ($statement->getTransactions() as $transaction) {
                $bookingDate = $transaction->getBookingDate();
                $bookingDate->setTimezone(new \DateTimeZone("Europe/Berlin"));

                $transfer = new PaymentTransfer();
                $transfer->setPaymentMethod($paymentMethod);
                $transfer->setPaymentTime($bookingDate);
                $transfer->setName($transaction->getName());
                $transfer->setAccountName($transaction->getAccountNumber());
                $transfer->setAccountCode($transaction->getBankCode());
                $transfer->setBookingText($transaction->getBookingText());
                $transfer->setDescription($transaction->getDescription1());

                $amount = ($transaction->getCreditDebit() === "debit") ? (-1) * $transaction->getAmount() : $transaction->getAmount();
                $transfer->setAmount($amount);

                $transfers[] = $transfer;
            }
        }

        return $transfers;
    }

    /**
     * This function is key to how FinTS works in times of PSD2 regulations. Most actions like wire transfers, getting
     * statements and even logging in can require a TAN, but won't always. Whether a TAN is required depends on the kind of
     * action, when it was last executed, other parameters like the amount (of a wire transfer) or time span (of a statement
     * request) and generally the security concept of the particular bank. The TAN requirements may or may not be consistent
     * with the TAN that the same bank requires for the same action in the web-based online banking interface. Also, banks
     * may change these requirements over time, so just because your particular bank does not need a TAN for login today
     * does not mean that it stays that way.
     *
     * The TAN can be provided it many different ways. Each application that uses the phpFinTS library has to implement
     * its own way of asking users for a TAN, depending on its user interfaces. The implementation does not have to be in a
     * function like this, it can be inlined with the calling code, or live elsewhere. The TAN can be obtained while the
     * same PHP script is still running (i.e. handleTan() is a blocking function that only returns once the TAN is known),
     * but it is also possible to interrupt the PHP execution entirely while asking for the TAN.
     *
     * @param \Fhp\BaseAction $action Some action that requires a TAN.
     * @param string $tan
     * @return string
     * @throws \Fhp\CurlException
     * @throws \Fhp\Protocol\ServerException
     */
    function handleTan(\Fhp\BaseAction $action, string $tan)
    {
        global $fints, $url, $bankCode, $username, $pin, $productName, $productVersion;

        // Find out what sort of TAN we need, tell the user about it.
        /*
        $tanRequest = $action->getTanRequest();
        echo 'The bank requested a TAN, asking: ' . $tanRequest->getChallenge() . "\n";
        if ($tanRequest->getTanMediumName() !== null) {
            echo 'Please use this device: ' . $tanRequest->getTanMediumName() . "\n";
        }

        // Challenge Image for PhotoTan/ChipTan
        if ($tanRequest->getChallengeHhdUc()) {
            $challengeImage = new \Fhp\Model\TanRequestChallengeImage(
                $tanRequest->getChallengeHhdUc()
            );
            echo "There is a challenge image." . PHP_EOL;
            // Save the challenge image somewhere
            // Alternative: HTML sample code
            echo '<img src="data:' . htmlspecialchars($challengeImage->getMimeType()) . ';base64,' . base64_encode($challengeImage->getData()) . '" />' . PHP_EOL;
        } */

        $tanFilePath = "resource://Bla.Blubb/Private/Documents/FintTSState.txt";

        // Optional: Instead of printing the above to the console, you can relay the information (challenge and TAN medium)
        // to the user in any other way (through your REST API, a push notification, ...). If waiting for the TAN requires
        // you to interrupt this PHP session and the TAN will arrive in a fresh (HTTP/REST/...) request, you can do so:
        if ($optionallyPersistEverything = true && $tan === "") {
            $persistedAction = serialize($action);
            $persistedFints = $fints->persist();

            // These are two strings (watch out, they are NOT necessarily UTF-8 encoded), which you can store anywhere.
            // This example code stores them in a text file, but you might write them to your database (use a BLOB, not a
            // CHAR/TEXT field to allow for arbitrary encoding) or in some other storage (possibly base64-encoded to make it
            // ASCII).
            file_put_contents($tanFilePath, serialize([$persistedFints, $persistedAction]));

            return "persisted";
        }

        //echo "Please enter the TAN:\n";
        //$tan = trim(fgets(STDIN));

        // Optional: If the state was persisted above, we can restore it now (imagine this is a new PHP session).
        if ($optionallyPersistEverything && $tan !== "") {
            $restoredState = file_get_contents($tanFilePath);
            list($persistedInstance, $persistedAction) = unserialize($restoredState);
            $fints = new \Fhp\FinTsNew($url, $bankCode, $username, $pin, $productName, $productVersion, $persistedInstance);
            $action = unserialize($persistedAction);

            $fints->submitTan($action, $tan);

            return "loaded";
        }

        return "sonstiges";

       // echo "Submitting TAN: $tan\n";
       // $fints->submitTan($action, $tan);
    }

    public function accountTransfers(): void
    {
        // TODO: Implement accountTransfers() method.
    }
}

I start the request ($tan = "") and Postbank sends me the related TAN. I submit everyhing again ($tan != ""), but then at "$fints->execute($getSepaAccounts);" an error is thrown: "Need to login (DialogInitialization) before executing other actions" in Line 266 in Packages/Libraries/nemiah/php-fints/lib/Fhp/FinTsNew.php

I think I haven't implemented something correctly from your example yet, but I can't figure it out for the best. Do you have a spontaneous idea? Thank you very much for your support!

CnczubehoerEu avatar Apr 15 '20 18:04 CnczubehoerEu

You modified handleTan() to have a return value, but you're not handling that in if ($getStatement->needsTan()).

Philipp91 avatar Apr 15 '20 18:04 Philipp91

Also, I don't understand how you call the script again once you have the TAN. You shouldn't call login() again, you shouldn't create a new GetStatementOfAccount instance either. Instead, you should just deserialize the ones you had before, just as if the code had continued right after the echo "Please enter the TAN:\n"; line from the example.

Philipp91 avatar Apr 15 '20 18:04 Philipp91

Thank you very much for your prompt reply. Oh yes, very stupid mistake. I splitted "handleTan" in two new functions "serializeAction" and "unserializeAction". Now it works great. Thank you very much again.

CnczubehoerEu avatar Apr 15 '20 19:04 CnczubehoerEu