mollie / mollie-api-php

Mollie API client for PHP
http://www.mollie.com
BSD 2-Clause "Simplified" License
552 stars 191 forks source link

No way to set a cancel URL when creating a payment #18

Closed robertotremonti closed 1 year ago

robertotremonti commented 8 years ago

When you create a payment, you need to set a redirectUrl parameter. Customer is redirected to that URL when purchase is completed. The same URL is used for Back to website button if you want to cancel payment process and go back to store. "Success" and "Cancel" URLs are different most of the times. How do I set a cancelUrl? If it's not possible I ask to add this functionality in next releases.

Thanks.

screen shot 2015-12-11 at 14 57 55

ghost commented 8 years ago

Hello Roberto,

When a customer returns to your website (when the customer clicked "Back to the website"), your webhook gets called to inform your system that the customer has aborted the transaction.

In your webhook (or this example) you can specify what should happen when this happens:

if ($payment->isPaid() == TRUE)
{
    /**
     * At this point you'd probably want to start the process of
     * delivering the product to the customer.
     */
}
elseif ($payment->isOpen() == FALSE)
{
    /**
     * The payment isn't paid and isn't open anymore.
     * We can assume it was aborted.
     */
}

Ricardo, Mollie

robertotremonti commented 8 years ago

Thanks, Ricardo, for your reply. I don't use a webhook. I think that a simple "CancelURL" could be introduced in next updates, like PayPal and other payment methods offer.

ghost commented 8 years ago

Hello Roberto,

We are currently discussing this internally. Thank you for your report.

Ricardo, Mollie

veloxy commented 8 years ago

What's the status on this? It makes sense to have success and cancel URL's, all the payment gateways I've used have them. Since the webhook is called in the background, we're never 100% sure the payment status is updated when the user is redirected, or am I missing something?

whvandervelde commented 8 years ago

You can check the status of the payment with mollie on your return url (as well as the webhook) and redirect users accordingly to succes or failure pages. Payment processed isn't guaranteed on the return url. So you do to give need proper feedback in case payment is still pending, but in my experience it is usually already processed. As a design decision I understand it, return the customer asap instead of waiting for payment to fail/succeed and only then returning. It avoids delays and otherwise why use a webhook (which is pretty useful for the event driven / asynchronuous)?

quannv commented 8 years ago

What's the status on this? I'm getting this problem of my customer, Hope you can update API soon

ghost commented 8 years ago

Hello everybody,

We did not change anything to our API regarding this. To read what has changed, see the changelog here.

My suggestion is to both use a returnUrl and a webhookUrl to get the most out of our API. The function of a returnUrl is whenever the user returns to your website, the status can be checked (If you developed that into your returnUrl). For payments that later change (eg. refund, chargeback) or delayed payments, I'd really suggest using the webhookUrl to get a correct status for a payment.

More about the status in our documentation. Also take a look at the webhook documentation.

I hope this helps.

Ricardo, Mollie

leongersen commented 8 years ago

This makes it impossible to use only the metadata feature when creating the payment: You'd have to retain the payment id to show a different page depending on whether the payment was cancelled.

1) The open amount for your purchase is €100, click to pay.
2) Customer returns to redirectUrl
3) Determine to show 'Thanks!' or 'Try again' => We'll need a paymentID
xewl commented 8 years ago

@leongersen It's not impossible without the webhook.

A payment is usually linked to a parent model (or object), which has its own identifier.

You can save the combination of the new payment object's id and the parent's identifier in a payments logging table and use its identifier. (eg. PaymentAttempts {'unique_id', 'model', 'model_id', 'payment_id', 'created', 'updated'})

This would need 2 queries:

  1. Before requesting a new payment:
    • create a payment log
    • grabbing its identifier (unique_id)
    • appending/including the identifier to the redirectUrl
  2. After the payment request:
    • saving the payment id to the previously generated log identifier
    • redirecting to the paymentUrl

When a page hit occurs on the dynamic page defined in the redirectUrl, after grabbing the log's identifier from the request, you can identify the payment being made. Which now results in:

3) Determine to show 'Thanks!' or 'Try again' by:
- grabbing the payment identifier from url/request (or fail in some way, I'd suggest 418 Teapot, jk..)
- grabbing the logged payment data we added before.
- collecting payment data from Mollie
- (updating a status or some sort on the model itself)
- (track a backend conversion or two)
- redirecting to a success or fail page (or wherever and send a flash message along) depending on the payment's status
- (track some front-end conversion pixels etc)

There's simpler ways, but this would be very effective.

The webhook isn't all that bad though: using the log's identifier inside metadata could be great for future updates on the same object. I personally combine the two.

Think forward.

leongersen commented 8 years ago

@xewl: Yes, that was my point. You can't implement this functionality without storing the paymentId on the application side. This renders the metadata feature almost entirely useless.

Parcye commented 8 years ago

You can add custom values to the return URL . I would opt for encryption. I have seen this in some documentation.

Return URL would be /mollie-return.php?hash=abcdef&val=12345 check if val + key match hash and you can then use that to access a locally stored record of the transaction ID . Or go for a 256bit? encryption of the transaction ID and pass that in the parameter.

xewl commented 8 years ago

@Parcye The identifier on the log table can be any "encrypted" code you'd want, but is as unique as it can get for an attempt itself, whatever value you tend to push in here: be it a primary key, or a random generated key.

It's just a matter of identifying it afterwards.

@leongersen Well... without the paymentId you aren't able to identify it this way, and thus you'd have to do it anyway. It's a better practice to log as even the webhook only receives that value. The rest is for you to grab and collect.

Long story short: If you don't log your paymentId at all, anywhere.., you dun goofed (:

mollierobbert commented 8 years ago

Hey guys, just wanted to chime in to see what we can do at Mollie to clear things up.

As stated earlier, we use a webhook system to send you status updates about your payments. We explicitly do NOT support a cancelUrl or any other type of status dependent URL, because it introduces various security issues that our webhook system has solved.

Just set up your payment with a webhook URL where we can push the status to. Send your consumer off to the gateway URL we provide. Then we will send your consumer back to the redirect URL you've provided once they either cancel or complete the payment. In both events, we will generally have called your webhook URL right before the consumer returns to your website, allowing you to show a proper status message to your consumers.

In short: we will never implement a cancelUrl because it goes entirely against our API design. Please feel free to ask us more about our webhook system if you still have questions.

xewl commented 8 years ago

It's true that the webhook call is mostly on time for that action, and can be processed on time. However when it isn't you're bound to wait or fail.

@mollierobbert Does the webhook have any connection (in time) to the payment gateway? Is it called during the payment process, is there a timeout? Does it also call when a user cancels via the button on the bottom?

I guess their concern is that they're not able to process the webhook on time to show a correct message, or work this out without the webhook at all.

I'd say that the only option to implement it this way, would be to always show the fail/cancel-page, unless a webhook has been called before to validate Payment->isPaid(), and thus show the Success-page. That does leave some space for error though, not much, but the chances are for real.

mollierobbert commented 8 years ago

It is in everyone's best interest for us to send you a status report via webhook before the consumer is sent back to your website. We try our best to do so.

However, sometimes things go wrong. The iDEAL issuer may experience a minor hiccup, for example. We don't want to delay the consumer too long, so while we wait for the bank's final status, we'll just send the consumer back to you.

You won't have received a webhook yet, so you'll just have to tell your consumer that the payment status is still unknown, but you're expecting to receive the payment soon.

This is the absolute best we (you as merchant and Mollie as payment service provider) can do. We cannot tell the consumer something we don't know.

We should definitely not hold the consumer hostage while we wait for the bank's status report, which may take minutes. The consumer may have already closed the browser, thinking the payment failed entirely. A status based URL system tends to introduce these types of problems to your payment flow.

Does the wehook have any connection (in time) to the payment gateway?

As stated above, we do our best to send the webhook before we redirect the consumer back to you.

Is it called during the payment process, is there a timeout?

Yes, we usually receive the payment status from our issuer directly, call your webhook, then issue the redirect. There is no timeout, because we don't need one. Your server will usually have processed the payment request in milliseconds, while the consumer's browser has probably not even received the redirect response yet.

Does it also call when a user cancels via the button on the bottom?

Yes. Actually, this is the only real way we know a consumer cancelled a payment. We can't measure a browser closing. We can only detect the consumer pressing the cancel button to return to your website, in which case you get a webhook for status=cancelled right before the consumer returns to your website.

I guess their concern is that they're not able to process the webhook on time to show a correct message, or work this out without the webhook at all.

Our API relies heavily on the webhook system. We highly advise you to use it.

We also highly advise storing the Mollie payment ID in your system for future reference and to be able to process the webhook reports easily.

I'd say that the only option to implement it this way, would be to always show the fail/cancel-page, unless a webhook has been called before to validate Payment->isPaid(), and thus show the Success-page.

Please just implement a 'pending' page. You can't always tell the consumer the payment failed or cancelled immediately. This is especially true with bank transfer. You will always want to design your system as such, that it confirms any payment status change via email.

Your consumers are used to receiving a final status via email. Going against that will be counterintuitive and somewhat suspicious to your consumers.

leongersen commented 8 years ago

@mollierobbert What your are saying is entirely true, and I wasn't attempting to debate this.

We're only updating our payment status on the webhook, never on the customer visit. This is an asynchronous process, and it doesn't really matter if this happens withing seconds, minutes or hours after a payment is made. Depending on the webhook to be invoked before the customer returns would only create race conditions.

In the case that the customer clicks cancel, however, this doesn't have anything to do with timeouts or latency in the connection with the bank. The customer just wants to return to the original site. I'd like to be able to say: "You've cancelled your payment. Click here to try again." As I see it, clicking cancel isn't a payment state, it is a guarantee that no payment was made. If the customer closes the browser before making a payment, they themselves know that no payment was made. They can return to the payment process by the same flow they used to get to it the first time.

What I'm trying to address is that it doesn't matter to our application that the payment was cancelled, we don't store or handle this state. We'd just like to show the customer a page that specifically acknowledges that they cancelled.

xewl commented 8 years ago

@mollierobbert That was the most beautiful piece of text I've read regarding this issue and now I have to go & add e-mail templates to my processing-queue. (Oops)

I fully understand the pending message. Thus. When the webhook has been called, act accordingly, or show the pending status. Should work 💯

mollierobbert commented 8 years ago

@xewl Glad to hear I could be of help :-).

@leongersen You're making a fair point I hadn't considered above. However, since the webhook system is already in place, we would still prefer to communicate the cancel status via webhook, since all payment status processing logic would already be present there in your system. Introducing a cancel URL just for this scenario, next to our regular webhook system, would undoubtedly confuse our less technical merchants.

Also, consider the consumer may return via the browser's back button, in which case the payment will remain in an open state. There's nothing we can do about that. (This is not a valid argument against the cancelUrl, just another example of why you shouldn't care too much about non-complete payment states.)

leongersen commented 8 years ago

@mollierobbert Thank you for your response. The argument it might confuse our less technical merchants seems like a very valid reason not to implement this feature. I hadn't really considered that viewpoint.

As an example for others, I've implemented the return page as something like:

Welcome back!

If you completed your payment successfully, you'll receive a confirmation email from us in a couple minutes. You can also -start the payment again-.

leongersen commented 8 years ago

On a sidenote, do you have data on what percentage of payments is explicitly cancelled?

mollierobbert commented 8 years ago

@leongersen Thanks for your suggestion. That would indeed be a proper message to show consumers if you did not receive a webhook call from us in the meantime.

Do you have data on what percentage of payments is explicitly cancelled?

In recent stats, only about 2% of payments are explicitly cancelled via the cancel button.

raphaelsrodrigues commented 7 years ago

@Ricknox we are having the same problem.

I agree with @robertotremonti - a simple CancelURL could be introduced.

mollierobbert commented 7 years ago

@raph8888 Please read the full thread above for a detailed explanation on why we will not support a cancelUrl parameter.

ndf commented 7 years ago

@mollierobbert I understand your reasoning, but we do have a problem to get cancelling working well with Drupal Commerce. The Commerce Payments API expects that payment providers do have a configuration option for a 'cancel url'. This has been chosen based on a broad analysis of popular Payment Provider API's.

This is a cross-link to the Drupal issue: https://www.drupal.org/node/2908280

mollierobbert commented 7 years ago

Sad to hear the Commerce Payments API is pushing for a URL based feedback system, Niels. As stated above, Mollie won't introduce a cancel URL. It may solve your particular issue, but will result in a vague API that Mollie will have to live with for years to come.

I'd advise to push the Commerce Payments API developers to mark the cancel URL as an optional feature.

If all else fails, since you're stuck between two APIs with different workflows, a last resort could be for you to introduce a layer inbetween Mollie's API and the Commerce Payments API. The middleware would then catch all webhooks and would redirect consumers to the appropriate status URL based on the async webhook status received from Mollie.

mattbrailsford commented 4 years ago

I know this is an old / closed issue, but just thought I'd express my desire for a cancelUrl also.

As an ecommerce platform developer our product is able to integrate with a number of payment gateways for which we aim to provide a consistent checkout flow across them all. This can be fairly difficult based on differences of those payment gateways, but the gateways that integrated the best, and a pattern we are seeing a lot is when they use 3 URLs.

Now I do understand the importance of using a webhook Url for status updates and you should NEVER use the redirect Url or cancel Url to update an order status, but from an actual user flow perspective having a cancel Url property is really useful in being able to direct visitors to the most relevant location.

I've read the suggestion of having an interim controller to try and figure out the location to send someone, but this seems like a really hacky way of doing it as it has a high potential to fail if the webhook hasn't hit yet, and also wastes a lot of requests having to fetch the order to check it's status and then perform another redirect when a simple straight redirect from the gateway could do that in one.

I do hope you might re-consider this decision in the future as it would make integration a whole lot simpler.

Cangoo commented 3 years ago

Just wanted to support mattbrailsford: The integration of Mollie in our own php-shop-system really went fast and smooth until now, when I came over the problem to distinguish between "user was redirected, because he completed the payment-process" (although I know this doesn't mean the payment completed sucessfully) and "user was redirected because he clicked the button 'back to webpage'". It's just a point to see from the users view: Did he want to complete the payment or not?

Out customer for who we do the implementation is now thinking about cancelling the subscription to Mollie, because we can't guarantee that a user which wanted to completed the payment might be redirected to the wrong page, because the webhook might not be called at the time the redirection happens.

mollierobbert commented 3 years ago

Thanks for chiming in, Matt and Cangoo.

Since the request keeps coming up, I'll bring up the discussion internally again as well.

We do of course want to help with payment gateway standardization efforts, and improve the consumer experience. The question remains to what extent the parameter would complicate the ease of use of our API.

To be continued.

dailyenergy commented 3 years ago

Big fan of Molly in regards to ease of use for clients and their customers. And looking forward to implementing Molly in a Umbraco site using Vendr by @mattbrailsford so please reconsider implementing the cancelUrl

mattbrailsford commented 3 years ago

@mollierobbert did this get raised in an internal discussion? Were there any new updates on this? We've had a handful of customers asking us to integrate which we'd love to do but so far we are just having to say it's dependent on the outcome of this issue.

sandervanhooft commented 3 years ago

I've used Mollie in a bunch of integrations and this has never caused any problems tbh.

Generally I let Mollie redirect back to something like a RedirectFromMollieController. Typically it's a simple class that decides what message to show the customer or what route to redirect to based on the payment status.

sandervanhooft commented 3 years ago

Using a dedicated RedirectFromMollieController means you can keep using your existing cancelUrl, and redirect there if the payment status is canceled. Does that help?

sandervanhooft commented 3 years ago

It may seem like it's a bit more resource expensive but practically I've never seen it cause any performance / UX issues.

mattbrailsford commented 3 years ago

@sandervanhooft sure, but that's much more susceptible to errors. The truth is, Mollie knows instantly that the return to the site from a cancel is because they clicked the cancel button. Without the use of a cancelUrl the only way for devs to work that out is to redirect to a middle man controller (your RedirectFromMollieController) and in that check to see if the webhook has been triggered yet and if it's not (maybe due to network delays) what then? wait? redirect to a "I don't know page". All of this just seems an excess of logic to know that the user clicked the cancel button.

Timollie commented 3 years ago

Hi @mattbrailsford, Thank you for your input. We are still discussing this point internally and will update this thread accordingly once we have reached a decision.

Most of the integrations I have seen indeed implement a redirectFromMollieController equivalent like Sander mentions. In a case in which the webhook has not been called yet (note this is actually very rarely the case), you can then retrieve the payment status from our API same as what you do when you receive the webhook. Based on that you can always display the returnpage corresponding to the most recent status of the payment.

A cancelUrl would only make the handling of statusses in case of a cancel easier, for all other statusses you will still need a redirectFromMollieController equivalent anyhow.

mollierobbert commented 3 years ago

I'd just like to add that the 'I don't know page' is required either way, since the consumer may return from the supplier while the payment is still in an 'open' state because of delays between the supplier and Mollie, or between Mollie and the merchant server.

As pointed out a few times in this thread, we're talking edge cases here. If you want to cover all the edge cases (which you should) then the 'I don't know page' cannot be avoided, regardless of whether Mollie offers a cancelUrl.

mattbrailsford commented 3 years ago

I do understand that an "I don't know" page is still needed, but my point is, and always has been, for the very specific "cancel back to the store" button click, the status is absolutely known at the point of redirect so why require all this hoop jumping to figure this out, and, potentially send them to the "I don't know" page if there are possible network issues either from the webhook request, or the attempt to get the status.

kpellegr commented 2 years ago

It looks like this is still an issue? I want to integrate Mollie into a Jotform checkout form. But when the user cancels the payment, the form is submitted anyway and the users gets a 'thank you' page. This is obviously not usable in a real-world scenario. Please advise...

Cangoo commented 2 years ago

Before showing the thank you page, you can check if the order was paid, authorized, completed or pending -> then show the thank you-page. If it's not one of these states, redirect to the previous page (perhaps with a message like: you cancelled the payment or an error occurred, please try again). We had doubts here as well but it works fine. It's only a pity that you can't tell the customer if he cancelled or an error occurred (both is possible I think) which might be a bit confusing.

toonvandeputte commented 2 years ago

Hello @kpellegr :)

Our approach to this is to check the payment status on the redirectUrl page, and show an appropriate message there. This is separate from the webhook call, and a canceled status should be available immediately if you fetch the payment object from Mollie based on the transaction ID. It does mean a slightly superfluous call to the Mollie API, but after about 4 years this hasn't given us any issues so far, and it's afaik the only way to provide proper feedback to the user.

As to the desirability of a cancelUrl, I'm not sure that would be a good idea. I think it's tricky to imply any sort of payment status from the url a user visits, I'd always check the Mollie API for this (or await a webhook call).

mollierobbert commented 2 years ago

Generally, you will almost always get a webhook before the consumer lands on the return page, so no additional API calls are needed per se within the return page logic.

Our system is explicitly designed to first process the payment status, then send you the webhook, and then send the consumer back to you. If either of the first two steps takes too long we may send the consumer back to you earlier, but this is relatively rare.

For those cases where you did not yet receive the webhook, a 'we are processing your payment' message works fine in most implementations.

But indeed @toonvandeputte, you can call our API directly for the rare case where the final state is already known to us, yet we did not send the webhook, but we did send the consumer to you. It's a good fallback for this specific case.

Still, please be mindful that the final status may not be known yet if the consumer returns. Integrations will always have to support the 'we are still processing your payment' flow. (Similarly, the consumer may complete the payment and not return at all. The webhook thus remains essential.)

Back to the cancelUrl — we recognize that adding this parameter will provide a short path for the consumer to cancel the payment and jump back to their shopping cart to make some changes.

As stated earlier, we also recognize that such a parameter is common in other payment gateways, and Mollie not offering it is causing friction in payment gateway standardization efforts.

We are investigating how we would implement this into our core systems, but that's all I can say at this point.

Perhaps superfluously, I want to mention the cancelUrl is not going to be a silver bullet. Not all payment methods support the dedicated 'cancel' path, so depending on what method the consumer chooses and how far they got into the payment process, the payment may still end up being failed. Not to mention the case where the consumer takes too long and the payment ends up expired. In those cases the user may still want to get back to the cart.

So, continuing to support shopping cart recovery after a non-paid webhook remains vital, even if we do end up introducing the cancelUrl for above-mentioned reasons.

sandervanhooft commented 2 years ago

Closing this for now, let me know if it should be reopened. The Mollie team (including @mollierobbert ) is aware of this feature request.

mollierobbert commented 1 year ago

Question to everyone who asked us to implement this...

We see most PSPs offer either one of the following three flavors:

Very few players have implemented both a catch-all URL and an explicit 'cancel URL' for only the cancel case. This makes us unsure about the best way for us to design this.

So in other words, the options we are considering are:

  1. Implement a 'cancel URL' only for the case where a consumer explicitly cancels the payment. The 'redirect URL' remains the catch-all URL for all other cases, including failures due to for example payment method supplier outages.
  2. Or, implement a 'cancel URL' as effectively a failure URL. This means whenever the cancel URL is configured, the regular 'redirect URL' effectively turns into a success URL.

Any thoughts from this audience on which one would be preferable?

Perhaps this is a semantical discussion, but better safe than sorry.

sandervanhooft commented 1 year ago

My 50 cts, happy to elaborate more if it helps.

I'd expect the redirectUrl to remain the default / catch-all, then have dedicated redirectUrlOnStatusX'es (Canceled/Failed/...) for specific overrides.

This way current integrations will not break functionally, and new specific redirectUrls can be added in multiple iterations.

I.e.

$payload = [
  'redirectUrl' => 'https://www.sandorian.com', // catch-all
  'redirectUrlOnCancel' => 'https://www.sandorian.com/canceled',
  'redirectUrlOnFail' => 'https://www.sandorian.com/failed',
];

Alternatively: use a structured object, similar to the amount object. When the redirectUrl is a string, use The Old Way. When an object is sent, handle The New Way.

// Support this:
$redirectUrl = 'https://www.sandorian.com';

// And this:
$redirectUrl = [
  'canceled' => 'https://www.sandorian.com/canceled',
  'failed' => 'https://www.sandorian.com/failed',
  'default' => 'https://www.sandorian.com', // catch-all
];

Alternatively for the object approach, use a different name, i.e. redirectUrlSettings (in lack of better inspiration).

Kimmax commented 1 year ago

Running into this exact scenario right now.
Reading the issue it seems like 99% of us really only care about the explicit canceled case, so @mollierobbert's

Implement a 'cancel URL' only for the case where a consumer explicitly cancels the payment. The 'redirect URL' remains the catch-all URL for all other cases, including failures due to for example payment method supplier outages.

would be our +1.
Will be going with the controller in the middle for now.

SDanDyS commented 1 year ago

@Kimmax This would only be a short term solution. Eventually people will ask for specific URLs for each status for whatever reason and they'll have to implement the other desires again. Imo, @sandervanhooft his solution is the best. Give a catch-all URL, and implement for each status an endpoint, if wished.

mollierobbert commented 1 year ago

We discussed the object notation internally, but decided to stick with a dedicated string parameter cancelUrl for now. If we ever introduce another status-based parameter, we can fold them all into an object which would then make more sense.

mollierobbert commented 1 year ago

The cancelUrl parameter has now been added to the Payments API. Please feel free to test it and share feedback either here or on our Discord.

We will roll out support for the Orders API as well — probably early January.

Happy holidays!

Cangoo commented 1 year ago

Works like charm, thank you!

Imar commented 1 year ago

Any chance the same URL will be added to Payment Links? When we send people to a payment link page (from a QR code on an invoice for example). I like to control the Previous Website link in the bottom left of the screen:

image

Thanks

Imar