avatar

Michak

full stack developer

iDEAL PHP/Laravel integration

iDEAL is one of the most popular e-commerce payment systems in Netherlands, in 2014 it was used for 54% of all Dutch online payments[1]. However although that this payment system is so popular it’s not so easy to implement, especially if you will need to implement it to some kind of fintech project which is obligated by law to not use any 3rd party providers like Mollie to process your clients payments. I was working on one of such projects and as I didn’t found much documentation in English on the integration in PHP I would like to make it easier for you.

NOTE: This solution has been tested only with Laravel framework, but I’m sure that it will work in any other modern PHP framework.

SlmIdealPayment

First thing we need to do is to install slm/ideal-payment package with composer, you can also download source code from Github repository here. You can be curious why I recommend you to use this not updated from almost 5 years package, but the reason is because for me it’s the only known PHP package which works with the newest version of iDEAL system. It have so simple code that if you are experienced PHP developer you shouldn’t have huge problems to fork it and fix or enhance it.

As you probably noticed this package is made for ZF2, but it can be easily used with other frameworks, like in this case with Laravel. In the package readme you can also read that it’s supports only ING Bank, Rabobank and ABN Amro, personally I tested it only with the last one, but it should work with first two without any problem too.

NOTE: You can have problems with installation this package with composer, as it’s still marked as beta, to get around this problem you can set "minimum-stability": "beta" in composer.json file.

Next we need to generate certificates for iDEAL, I think the best place to put them will be our /storage directory, so let’s create certs/  folder inside of it run these two commands to generate private key and certificate. In place of [privateKeyPass] put your own password for private key and don’t try to generate certificate for

openssl genrsa –aes128 –out priv.pem –passout pass:[privateKeyPass] 2048

openssl req –x509 –sha256 –new –key priv.pem –passin pass:[privateKeyPass] -days 1825 –out cert.cer

ABN Amro configuration

If you already have generated certificate for your server, you need to upload it to your merchant dashboard. In case of ABN Amro, you can find it in the same part of dashboard where you can find your merchant ID, just switch to Security tab, where you can find file input. Select your .cer file generated earlier and upload it, if there is something wrong with you certificate you will get clear message about it, for example there will be error if your message expiration date is more than 5 years in future.

After upload you should see your certificate fingerprint in table.

Last thing you need to do is download ABN Amro certificate, you can found it in the Documentation page of your dashboard. You can find there file called ABN AMRO public key…, download it and put next to your cerificate in your application, you change it name to for example abnamro.cer, as I did.

Application configuration

Let’s now focus on our application code, firstly we can create config/ideal.php file, where we will create new values for Laravel config function:

<?php

return [
    'url' => env('IDEAL_URL'),
    'merchant_id' => env('IDEAL_MERCHANT_ID'),
    'key_password' => env('IDEAL_KEY_PASSWORD')
];

Now as you probably as you probably guessed we need to set our environment variables to .env file

IDEAL_URL=https://abnamro-test.ideal-payment.de/ideal/iDEALv3 // this is test URL for ABN Amro, for ING and Rabobank you should be able to found it it docs
IDEAL_MERCHANT_ID= // here you need to put your merchant ID, you should be able to find it in your iDEAL dashboard
IDEAL_KEY_PASSWORD= // this is password you used to generate your private key in previous step

Integration code

After you finish all configuration we should start coding iDEAL integration. It will be simple example of implementation where we will get available directories to show for user to select and after that we will redirect him/her to transaction website, after transaction we will be able to check if everything went fine.

Firstly let’s create for example directory structure Services/IDeal. Inside of this directory we should create standard client class which we will use in our other three classes to now repeat the same code three times. Let’s call it IDealClient.php and put this code inside of this file:

<?php

namespace App\Services\IDeal;

use SlmIdealPayment\Client\StandardClient;
use SlmIdealPayment\Options\StandardClientOptions;

class IDealClient
{
    protected $client;

    public function __construct()
    {
        $options = new StandardClientOptions;
        $options->setRequestUrl(config('ideal.url'));
        $options->setMerchantId(config('ideal.merchant_id'));
        $options->setSubId('0');

        $options->setPublicCertificate(storage_path('certs/abnamro.cer'));
        $options->setPrivateCertificate(storage_path('certs/cert.cer'));

        $options->setKeyFile(storage_path('certs/priv.pem'));
        $options->setKeyPassword(config('ideal.key_password'));

        $this->client = new StandardClient($options);
    }
}

This code should be easy to understand, in the class constructor we creating new $options variable which will be object with our client options. As you can see we used URL and merchant ID from config and provided paths to our certificates, if you put them in other directory then storage/  you should use something else then storage_path(…).

Now we should create file called IDealDirectoriesFetcher which we use to get directories available for our account.

<?php

namespace App\Services\IDeal;

use App\Services\IDeal\IDealClient;
use SlmIdealPayment\Request\DirectoryRequest;

class IDealDirectoriesFetcher extends IDealClient {
    public function getAll()
    {
        $directories = [];
        try {
            $request = new DirectoryRequest;
            $response = $this->client->send($request);

            foreach ($response->getCountries() as $country) {
                $directories[$country->getName()] = [];

                foreach ($country->getIssuers() as $issuer) {
                    $directories[$country->getName()][$issuer->getId()] = $issuer->getName();
                }
            }

            return $directories;
        } catch (\Exception $e) {
            \Log::error($e->getMessage());

            return $directories;
        }
    }
}

As you can see we extends IDealClient class, so we have access to previously created client instance. Firstly we need to get countries list available for our account, and after that we can get issuers to put in the directories array which we will use to show in select box.

Let’s go now to the more interesting files for you I guess. In the new file called IDealTransactionCreator you should put the code responsible for creating new transaction and sending request to iDEAL.

<?php

namespace App\Services\IDeal;

use App\Services\IDeal\IDealClient;
use SlmIdealPayment\Model\Issuer;
use SlmIdealPayment\Model\Transaction;
use SlmIdealPayment\Request\TransactionRequest;

class IDealTransactionCreator extends IDealClient
{
    public function create(array $data)
    {
        try {
            $issuer = new Issuer;
            $issuer->setId($data['issuer_id']);

            $transaction = new Transaction;
            $transaction->setPurchaseId($data['purchase_id']);
            $transaction->setAmount($data['amount']);
            $transaction->setDescription($data['description']);
            $transaction->setEntranceCode(str_random(7));

            $request = new TransactionRequest;
            $request->setIssuer($issuer);
            $request->setTransaction($transaction);
            $request->setReturnUrl(route('payment.confirm'));

            $response = $this->client->send($request);
            
            return $response;
        } catch (\Exception $e) {
            \Log::error($e->getMessage());

            return false;
        }
   }
}

As you can see in above example I’m using there array with information about purchase ID, amount, description, issuer ID, it should be simple for you, you can put in that array values which you will need, you need only to check if there will be valid, you shouldn’t for example send Lorem ipsum as amount. Issuer ID is of course ID of selected directory, which we obtain in previous step. As you can notice I’m using route to generate return URL, it’s the URL where user will be redirected after payment, you can put there what you need.

You probably already noticed that we not specified any return URL where iDEAL should send POST request with information about payment status as in some of payment getaways. We don’t need such URL, because we need to do separate request to iDEAL system which will return to us information about payment. Let’s create new file called IDealTransactionChecker.

<?php

namespace App\Services\IDeal;

use App\Mail\DepositIdeal;
use App\Services\IDeal\IDealClient;
use SlmIdealPayment\Model\Transaction;
use SlmIdealPayment\Request\StatusRequest;

class IDealTransactionChecker extends IDealClient
{
    public function validate($transaction_id) {
        try {
            $transaction = new Transaction;
            $transaction->setTransactionId($transaction_id);

            $request = new StatusRequest;
            $request->setTransaction($transaction);

            $response = $this->client->send($request);
            
            if ($response->getTransaction()) {
                return $response->getTransaction()->getStatus();
            } else {
                return false;
            }
        } catch (\Exception $e) {
            \Log::error($e->getMessage());

            return false;
        }
    }
}

As you can see we need to use transaction ID to get information about status of transaction. I have noticed that iDEAL, at least in development mode don’t return as much information, only simple status string which we can use to validate user’s payment.

Application controllers

I don’t want to show here again next whole files of code, I’ll but here only short code snippets which you can use to use previously created classes.

Firstly add classess to your controller class:

use App\Services\IDeal\IDealDirectoriesFetcher;
use App\Services\IDeal\IDealTransactionCreator;
use App\Services\IDeal\IDealTransactionChecker;

Next you can assign services to class instances with Laravel dependency injection:

    protected $idealDirectoriesFetcher;
    protected $idealTransactionCreator;
    protected $idealTransactionChecker;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct(
        IDealDirectoriesFetcher $idealDirectoriesFetcher,
        IDealTransactionCreator $idealTransactionCreator,
        IDealTransactionChecker $idealTransactionChecker
    ) {
        $this->idealDirectoriesFetcher = $idealDirectoriesFetcher;
        $this->idealTransactionCreator = $idealTransactionCreator;
        $this->idealTransactionChecker = $idealTransactionChecker;
    }

Now in first controller action for example called paymentForm you can fetch directories list, which you can use in view of this controller $directories = $this->idealDirectoriesFetcher->getAll();. In my case I created select box with directories and called it issuer_id. Let’s now create new action, called paymentProcess and put for example below code inside of it:

    public function paymentProcess(Request $request)
    {
        $data = $request->only(['issuer_id', 'amount']);
        $data['purchase_id'] = str_random(8);
        $data['description'] = 'Payment from user: ' . \Auth::user()->id;

        $transaction = $this->idealTransactionCreator->create($data);

        if ($transaction) {
            return redirect()->to($transaction->getAuthenticationUrl());
        } else {
            flash(__('ideal.payment-failed'))->error();

            return redirect()->route('payment.form');
        }
    }

As you can see I also created field called amount, and populated our data array with issuer ID and amount from form, randomly generated purchase ID and description with user ID. After we create transaction we have to redirect user to transaction URL, where he/she will be able to continue payment. After that we will need only one action, which will be our return URL where we validate our transaction and show success message to user. It can for example look like this:

    public function paymentConfirm(Request $request)
    {
        $orderStatus = $this->idealTransactionChecker->validate($request->get('trxid'));

        if ($orderStatus == 'Success') {
            return view('payment.success');
        }
        
        flash(__('ideal.payment-failed'))->error();

        return redirect()->route('payment.form');
    }

In the response from iDEAL we getting trxid which is nothing else then our transaction ID which we need to use to get status of our payment.

Production configuration

The only change between production and test configuration is URL of production environment, you should be able to find it at your payment provider website. You can use the same certificates which you uploaded to test account, merchant ID will probably also have the same value as in testing environment.

There is one tricky thing about production account setup, after you sign contract and do all formal steps to have fully functional iDEAL payments you will have to do some tests on the same account but still in testing environment. Probably you will do these tests already during development, but in case you use different account you will need to do 7 requests, one with directories fetch and 6 for payments with different statuses.

For mor information you can check iDEAL Checkout documentation, it’s in Dutch but you should understand it without any bigger problems, you can also always use translator.

After these tests you have to wait about 20 minutes, and after tests change statuses to passed you can change your URL to production URL.

Summary

That’s all what you need to start with iDEAL integration in PHP. In case you will have any problems please contact with me through comments, maybe I or someone else will be able to help you.

Thank you for reading.

NOTE: Because iDEAL is not popular in my country and I have experience with it only as a developer for clients from Netherlands, please don’t hestitate to correct my mistakes in this post in comments below

Featured image vector created by Freepik

NO COMMENTS

There are no comments in this post.

DROP A COMMENT

Your email address will not be published. Required fields are marked *