Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing the Mollie payment gateway #688

Closed
chocolata opened this issue Dec 22, 2020 · 26 comments
Closed

Implementing the Mollie payment gateway #688

chocolata opened this issue Dec 22, 2020 · 26 comments

Comments

@chocolata
Copy link
Contributor

Hi,

I'm trying to get the payment gateway Mollie working with the Mall plugin. I've created a plugin and imported the relevant OmniPay packages (https://github.com/thephpleague/omnipay-mollie). I'm basing myself on the example as provided by the makers of the OmniPay Mollie package.

I would like the user to be redirected to the Mollie payment page (example https://www.mollie.com/payscreen/select-method/8sTxQCJ7Vy) and have them select their preferred way of paying there (debit card, credit card, paypal, bank transfer...) and have them redirected to the site once they complete the payment.

My in my Payment Provider class, the process method looks like this:

    public function process(PaymentResult $result): PaymentResult
    {
        $gateway = \Omnipay\Omnipay::create('Mollie');
        $gateway->setApiKey('test_xxxxxxxxxxxxxxxxxxxxxxxx');

        $response = $gateway->purchase(
            [
                "amount" => "10.00",
                "currency" => "EUR",
                "description" => "My first Payment",
                "returnUrl" => "https://webshop.example.org/mollie-return.php"
            ]
        )->send();

        $data = (array)$response->getData();

        // Process response
        if ($response->isSuccessful()) {

            // Payment was successful
            print_r($response);

        } elseif ($response->isRedirect()) {

            // Redirect to offsite payment gateway
            $response->redirect();

        } else {

            // Payment failed
            echo $response->getMessage();
        }
    }

This almost works, but there is one big snag: it seems Mollie doesn't allow a redirect to their platform that originates in Ajax. So I get the following error message (both in localhost development and in a staging environment with a valid SSL-certificate):

Access to XMLHttpRequest at 'https://www.mollie.com/payscreen/select-method/8sTxQCJ7Vy' (redirected from 'http://localhost/myproject/checkout/confirm') from origin 'http://localhost' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

I did some searching and saw that others encounter the same issue (with different platforms):
https://stackoverflow.com/questions/43766286/no-access-control-allow-origin-header-is-present-on-the-requested-resource-m

They ask that the redirect happens via another way, for example the user pressing a certain button, or a redirect in another way.
What would you recommend I do in this situation? Is there a workaround for such a thing?

Thanks in advance for your response.

@chocolata chocolata changed the title CORS problem when being redirected to external payment page with custom payment gateway CORS problem when being redirected to external payment page with custom payment gateway (Mollie) Dec 22, 2020
@tobias-kuendig
Copy link
Member

Instead of using $response->redirect(); you can use $response->getRedirectResponse().

Full example, completely untested:

    public function process(PaymentResult $result): PaymentResult
    {
        $gateway = \Omnipay\Omnipay::create('Mollie');
        $gateway->setApiKey('test_xxxxxxxxxxxxxxxxxxxxxxxx');

        $response = $gateway->purchase(
            [
                "amount" => "10.00",
                "currency" => "EUR",
                "description" => "My first Payment",
                'returnUrl' => $this->returnUrl(),
                'cancelUrl' => $this->cancelUrl(),
            ]
        )->send();

		// This example assumes that if no redirect response is returned, something went wrong.
        // Maybe there is a case, where a payment can succeed without a redirect?
        if ( ! $response->isRedirect()) {
            return $result->fail((array)$response->getData(), $response);
        }

        Session::put('mall.payment.callback', self::class);

        return $result->redirectResponse($response->getRedirectResponse());
    }

    public function complete(PaymentResult $result): PaymentResult
    {
        $this->setOrder($result->order);

		// Using the purchase example from the mollie docs, you don't have to do anything here. 
		// Just return $result->success to mark your purchase as successful.
        // If you are using the "Order API" example, you would call $gateway->completeOrder here.

        return $result->success($data, null);
    }

@chocolata
Copy link
Contributor Author

chocolata commented Dec 22, 2020

Hi Tobias, thank you very much for wanting to help out. I appreciate it.

I've tried out your modifications, but unfortunately it throws the same error regardins CORS as before... Do you have another idea where the problem could be? I'd be more than happy to provide more details if you like. You can test the payment method out here: https://aircokopen.chocolata.be/categorie/webshop

Is there maybe a way in which we can update a partial on the checkout page with a piece of JavaScript that redirects the user?

P.S.: I double checked that the domains I'm using are allowed by Mollie - the CORS problem should be unrelated to the domains the sites are on now.

P.P.S: Here is the resource that states that Mollie does not support AJAX calls: https://help.mollie.com/hc/en-us/articles/214017905-When-I-call-the-Mollie-API-via-AJAX-I-receive-the-error-message-No-Access-Control-Allow-Origin-header-is-present-

@tobias-kuendig
Copy link
Member

If you never call $response->redirect() directly, there should be no AJAX call involved. By returning $result->redirectResponse($response->getRedirectResponse()); you essentially pass the redirect through to the Checkout component via the payment service:

return $this->redirector->handlePaymentResult($result);

and the component returns it from the onCheckout method:

return $paymentService->process();

which in turn should do a proper redirect, no AJAX request.

Can you share your full code?

@chocolata
Copy link
Contributor Author

chocolata commented Dec 23, 2020

Hi - thanks for your answer. I've checked, but I don't see any uses of $response->redirect(). Below is my code. I created a new plugin called MollieMall - in its vendor folder, I downloaded OmniPay and the OmniPay Mollie package via Composer. In the classes folder, I created a class named MollieMall.php, that looks like this (using your latest changes):

<?php
namespace Chocolata\MollieMall\Classes;

use OFFLINE\Mall\Models\PaymentGatewaySettings;
use Omnipay\Omnipay;
use Omnipay\Mollie;
use OFFLINE\Mall\Classes\Payments\PaymentResult;
use RainLab\Translate\Classes\Translator;
use Request;
use Session;
use Throwable;
use Validator;

class MollieMall extends \OFFLINE\Mall\Classes\Payments\PaymentProvider
{
    /**
     * The order that is being paid.
     *
     * @var \OFFLINE\Mall\Models\Order
     */
    public $order;
    /**
     * Data that is needed for the payment.
     * Card numbers, tokens, etc.
     *
     * @var array
     */
    public $data;

    /**
     * Return the display name of your payment provider.
     *
     * @return string
     */
    public function name(): string
    {
        return 'Mollie';
    }

    /**
     * Return a unique identifier for this payment provider.
     *
     * @return string
     */
    public function identifier(): string
    {
        return 'mollie';
    }

    /**
     * Validate the given input data for this payment.
     *
     * @return bool
     * @throws \October\Rain\Exception\ValidationException
     */
    public function validate(): bool
    {

        $rules = [

        ];

        $validation = \Validator::make($this->data, $rules);
        if ($validation->fails()) {
            throw new \October\Rain\Exception\ValidationException($validation);
        }

        return true;
    }

    /**
     * Return any custom backend settings fields.
     *
     * These fields will be rendered in the backend
     * settings page of your provider.
     *
     * @return array
     */
    public function settings(): array
    {
        return [
            'api_key'     => [
                'label'   => 'API-Key',
                'comment' => 'The API Key for the payment service',
                'span'    => 'left',
                'type'    => 'text',
            ],
        ];
    }

    /**
     * Setting keys returned from this method are stored encrypted.
     *
     * Use this to store API tokens and other secret data
     * that is needed for this PaymentProvider to work.
     *
     * @return array
     */
    public function encryptedSettings(): array
    {
        return ['api_key'];
    }

    /**
     * Process the payment.
     *
     * @param PaymentResult $result
     *
     * @return PaymentResult
     */

    public function process(PaymentResult $result): PaymentResult
    {
        $gateway = \Omnipay\Omnipay::create('Mollie');
        $gateway->setApiKey('test_xxxxxxxxxxxxxxxxxxxxxxxxxxx');

        $response = $gateway->purchase(
            [
                "amount" => "10.00",
                "currency" => "EUR",
                "description" => "My first Payment",
                'returnUrl' => $this->returnUrl(),
                'cancelUrl' => $this->cancelUrl(),
            ]
        )->send();

        // This example assumes that if no redirect response is returned, something went wrong.
        // Maybe there is a case, where a payment can succeed without a redirect?
        if ( ! $response->isRedirect()) {
            return $result->fail((array)$response->getData(), $response);
        }

        Session::put('mall.payment.callback', self::class);

        return $result->redirectResponse($response->getRedirectResponse());
    }

    public function complete(PaymentResult $result): PaymentResult
    {
        $this->setOrder($result->order);

        // Using the purchase example from the mollie docs, you don't have to do anything here.
        // Just return $result->success to mark your purchase as successful.
        // If you are using the "Order API" example, you would call $gateway->completeOrder here.

        return $result->success($data, null);
    }
}

My Plugin.php file looks like this

<?php namespace Chocolata\MollieMall;

use Chocolata\MollieMall\Classes\MollieMall;
use System\Classes\PluginBase;
use OFFLINE\Mall\Classes\Payments\PaymentGateway;

class Plugin extends PluginBase
{
    public function boot()
    {
        $gateway = $this->app->get(PaymentGateway::class);
        $gateway->registerProvider(new MollieMall());
    }
}

I made sure of configuring the payment gateway in the Mall settings:
image

This is what my plugin folder looks like:

image

Might this be of any help?

@tobias-kuendig
Copy link
Member

You can also try the method we use for paypal, use ->redirect() and fetch the URL manually from the $response:

https://github.com/OFFLINE-GmbH/oc-mall-plugin/blob/develop/classes/payments/PayPalRest.php#L68

        return $result->redirect($response->getRedirectResponse()->getTargetUrl());

If you submit the order, the response of the POST request should contain a X-OCTOBER-REDIRECT header. Check your devtools. The October JS Framework will pick this up and redirect the user client-side, without triggering any AJAX requests.

@chocolata
Copy link
Contributor Author

Hi Tobias, thanks so much. It does seem to work with your final suggestions. I'll do some more experiments and keep you posted. Thanks again and have a great Christmas!

@tobias-kuendig
Copy link
Member

Great! If you can confirm that it works we could integrate the final provider with the plugin. There have been multiple requests for a Mollie integration but there service is not known where I live so I never got around to come up with an implementation.

@chocolata
Copy link
Contributor Author

Hi Tobias, that would be so great. I've got a few days of holiday, but I'll try to get the Mollie integration up and running for 100% as soon as possible and share the code with you for review. I hope it will be up to standard. I'll keep you posted.

@PubliAlex
Copy link
Contributor

Mollie addition would be a really good news !

@Bensji
Copy link

Bensji commented Jan 3, 2021

Any update on this? Would be interested in this as well :).

@chocolata
Copy link
Contributor Author

Nice that there's some interest in this! I'll be working on a basic version of this this week and present it to Tobias by the end of it.

@chocolata
Copy link
Contributor Author

chocolata commented Jan 7, 2021

Hi Tobias, just to let you know that I've created a temporary plugin called MollieMall, ready for your evaluation here: https://github.com/maartenmachiels/molliemall

You can download the files of this repo to a folder named chocolata inside your plugins folder and it should work.
I have sent you an e-mail at info[at]offline[dot]ch with a test API-key that you should be able to use.

If you prefer to see it in action on another live site, you can visit: https://aircokopen.chocolata.be/product/kabelgoot-aluminium where the plugin is up and running.

Can you please review the code? Everything seems to be working, but there is still some issues:

  1. When clicking the back button on the bottom left of the Mollie payment screen, the store says that the payment was succesful while it hasn't been completed at all - maybe cancelled is a better status here?
  2. When a payment isn't succesful, or pending, the Mollie payment screen just refreshes and does not redirect back to the shop. (For example, when choosing bank transfer as a payment method, the screen refreshes when we choose the (test) payment status "open" or "cancelled"...)

Do you have an idea how we could tackle those things?

I chose not to use the Mollie Order API, since this requires the address to be split into very specific parts (street, zipcode and city all in separate fields) and this isn't available out of the box in the Mall plugin just yet.

You have my contact information by mail if you want to go over some details. I'd be more than happy to take some time out to share my (Mollie backend) screen with you if this would be useful.

Integrating into the Mall plugin core should be straightforward. In that case the Omnipay Mollie driver would have to be included, together with the MollieMall.php class from the above plugin folder.

Thanks again for all your help.

@tobias-kuendig
Copy link
Member

Thank you very much for your work! I will take a look at the code as soon as time allows.

When clicking the back button on the bottom left of the Mollie payment screen,

Did you set up the cancelUrl correctly?

When a payment isn't succesful, or pending, the Mollie payment screen just refreshes and does not redirect back to the shop.

Could this be something mollie specific? Does ist need another url besides the returnUrl and cancelUrl? Did you check out the Omnipay package's documentation on this. Maybe someone has opened an issue regarding this question against their repo?

@chocolata chocolata changed the title CORS problem when being redirected to external payment page with custom payment gateway (Mollie) Implementing the Mollie payment gateway Jan 11, 2021
@chocolata
Copy link
Contributor Author

Hi Tobias, thanks for your answer and thanks that you're willing to look at it when you find the time.

I've set up the cancelUrl here

I have to be honest that the payment mechanism is a bit unclear to me, so the problem surely will lie in my logic that handles the Mollie return.

There is a mention of a redirectUrl and webhookUrl in the Mollie docs though, but since the redirectUrl is required and the system does not throw an error message, I'm pretty sure this is handled by the Omnipay package. More info here:

I also saw this issue, making me think there might be some serious limitations to the Omnipay package

Thanks again. Looking forward to your views.

@Bensji
Copy link

Bensji commented Jan 11, 2021

FYI: I noticed that every payment comes back as successful, even when not in Mollie. So something is definitely not working yet. I'll check it out myself later too :).

@chocolata
Copy link
Contributor Author

Thank you Bensji - I'm starting to think more and more that we need to ditch the Mollie Omnipay package and use the Mollie PHP wrapper instead. When you say that every payment comes back as succesful, do you mean also in Stripe and other payment methods? Or is it only when using this plugin?

Thanks again for your support. If you'd like to talk more in-depth about this, you can contact me via my website https://chocolata.be and we could for example schedule an online meeting or something. I'd be more than happy to put in more work.

@chocolata
Copy link
Contributor Author

Hi guys, has anyone of you had the chance to look at this code? Thanks in advance.

@tobias-kuendig
Copy link
Member

tobias-kuendig commented Feb 7, 2021

I did add an implementation in the mollie branch based on your plugin, @maartenmachiels:
https://github.com/OFFLINE-GmbH/oc-mall-plugin/tree/mollie

It's important to fetch the payment after the user has returned to the store and check the status of it. If the user canceled the payment it will have the canceled status, for example. Bank payments will set it to open or pending and only credit cart payments mark it as paid.

Currently, this implementation is only one side of the story: If the user triggers a bank payment, the order status is changed to pending. Using Mollie's Webhooks, you would be automatically notified about a payment that becomes complete only after some time:
https://docs.mollie.com/guides/webhooks

Maybe someone is willing to add these features to the branch. Otherwise, there remains a manual step for the shop owner.

Please test this version and let me know if anything needs fixing.

@chocolata
Copy link
Contributor Author

Hi Tobias - thanks so much for your work. I downloaded the repo and ran composer install in it, to download the dependencies. Unfortunately, I'm getting the following error message constantly:

[Mon Feb 08 14:15:30.162951 2021] [php7:error] [pid 12408:tid 1876] [client ::1:50270] PHP Fatal error:  Declaration of Illuminate\\Cache\\Repository::add($key, $value, $minutes) must be compatible with Illuminate\\Contracts\\Cache\\Repository::add($key, $value, $ttl = NULL) in E:\\Dropbox\\Projects\\_xampp\\htdocs\\mollietest\\vendor\\laravel\\framework\\src\\Illuminate\\Cache\\Repository.php on line 239
[Mon Feb 08 14:15:30.175916 2021] [php7:error] [pid 12408:tid 1876] [client ::1:50270] PHP Fatal error:  Uncaught Error: Class 'October\\Rain\\Halcyon\\MemoryRepository' not found in E:\\Dropbox\\Projects\\_xampp\\htdocs\\mollietest\\vendor\\october\\rain\\src\\Halcyon\\MemoryCacheManager.php:12\nStack trace:\n#0 E:\\Dropbox\\Projects\\_xampp\\htdocs\\mollietest\\vendor\\laravel\\framework\\src\\Illuminate\\Cache\\CacheManager.php(154): October\\Rain\\Halcyon\\MemoryCacheManager->repository(Object(Illuminate\\Cache\\FileStore))\n#1 E:\\Dropbox\\Projects\\_xampp\\htdocs\\mollietest\\vendor\\laravel\\framework\\src\\Illuminate\\Cache\\CacheManager.php(105): Illuminate\\Cache\\CacheManager->createFileDriver(Array)\n#2 E:\\Dropbox\\Projects\\_xampp\\htdocs\\mollietest\\vendor\\laravel\\framework\\src\\Illuminate\\Cache\\CacheManager.php(80): Illuminate\\Cache\\CacheManager->resolve('file')\n#3 E:\\Dropbox\\Projects\\_xampp\\htdocs\\mollietest\\vendor\\laravel\\framework\\src\\Illuminate\\Cache\\CacheManager.php(58): Illuminate\\Cache\\CacheManager->get('file')\n#4 E:\\Dropbox\\Projects\\_xampp\\htdocs\\mollietest\\vendor\\laravel\\framework\\src\\Illuminate\\Cache\\CacheManager.php(304): Illuminate\\Cache\\ in E:\\Dropbox\\Projects\\_xampp\\htdocs\\mollietest\\vendor\\october\\rain\\src\\Halcyon\\MemoryCacheManager.php on line 12

When I remove the Offline folder from my plugins folder, the error message is gone (but replaced by an error message pertaining to the cart component not existing...). Could I have done something wrong on my end?

Is there something else I could do to test out the new functionality? Thanks in advance.

@tobias-kuendig
Copy link
Member

Make sure to run composer install in October's root directory, not the plugin folder. This makes sure, that you will get the right Laravel Framework depencendies installed.

@chocolata
Copy link
Contributor Author

Hi Tobias, thanks for your insights. After some fidgeting, I got the testsetup working. These are my findings:

When choosing a payment method and selecting any other payment status than "paid", the Mollie page just refreshes. I think when the payment is cancelled, failed or expired, the shop should display an according message.

One thing I saw, but cannot seem to reproduce is that clicking the back link at the bottom of the Mollie page did yield a succesful order. In my next tests, clicking this link threw the right error message => payment cancelled.

Could you kindly look into this? Did you receive the test api key I sent you via mail on January 7th? I'd be more than happy to do any further tests.

@tobias-kuendig
Copy link
Member

When choosing a payment method and selecting any other payment status than "paid", the Mollie page just refreshes. I think when the payment is cancelled, failed or expired, the shop should display an according message.

That's what I experienced as well. I wasn't sure if my AdBlcoker is interfering or another problem exists. It's strange that this seems to be default behaviour. The shop should show a message in these cases.

One thing I saw, but cannot seem to reproduce is that clicking the back link at the bottom of the Mollie page did yield a succesful order

It would be interesting to see what the $data variable contains if you have such a case (maybe log it using the info helper):

$data = (array)$response->getData();

For the success message to appear the status would have to be paid, pending or open. For pending and open the order's status would be set to pending payment. Maybe you can find the order that was created in that case? It might be possible that Mollie returns òpen state in certain circumstances?

Also, check the payment_logs table, it may contain the received response from the gateway where you can see the order status.

@chocolata
Copy link
Contributor Author

Hi Tobias,

Thanks for your answer. Here is some additional information on the payment statuses in Mollie:

  • Paid | Your customer successfully made a payment.
  • Open | A payment was started, but not finished.
  • Expired | If your customer doesn’t make an actual payment, the payment will at some point expire. The expiry time is different for each payment method.
  • Canceled | Your customer clicked on Cancel payment.
  • Failed | The payment failed. This may be due to a banking error.
  • Settled | The money is transferred to your business account.
  • Chargeback | Your customer rolled back a credit card payment or a SEPA collection failed.
  • Refunded | You refunded the payment to your customer.
  • Partially refunded | You refunded a part of the payment to your customer.
  • Pending | The payment process started, but is not yet finalised. This status changes as soon as the payment is done.
  • Authorised | The payment is done, but the money will be transferred as soon as you send the order. This status is only available for customers that use Klarna Slice it or Klarna Pay later.

I feel like we are hitting the limits of the OmniPay Mollie library. Did you see these issues?

@tobias-kuendig
Copy link
Member

I haven't looked in the Mollie API directly, but if they provide a good SDK changing the payment provider should be trivial.

Would that problem be solved by handling the incoming webhooks? In case of a pending payment, a webhook should be sent once they payment went through, right?

@chocolata
Copy link
Contributor Author

chocolata commented Mar 26, 2021

Hi tobias, sorry for the late reply... I've been dealing with some personal stuff but I'm getting back on the horse.

Yes, their API should be very straightforward. I think that the issue would indeed be solved by handling the incoming webhook. Indeed, the pending payment would be updated to "Paid" or "Failed" when processing by Mollie is finished.

Here you can see the payments documentation and a graph of how Mollie works in English (documentation is also available in German) https://docs.mollie.com/payments/overview

Ah, and by the way, there are two packages available that can be used for implementing Mollie:

Is there maybe any way we could discuss this one on one? I'd be more than happy to do any work on this implementation, but I do feel I'd need some guidance on getting it up and running within the OFFLINE Mall Plugin. Would you feel up to that? If so, how would you like me to contact you?

@tobias-kuendig
Copy link
Member

I'm closing this issue due to inactivity. If you need any further assistance feel free to reply.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants