A request for a future PaymentProcessor change

11 years ago
Hi

I've now written two PaymentProcessor implementations and both of them have hit very similar challenges.

Essentially there are some issues with the Nop flow which don't seem to work perfectly with external payment systems (e.g. with SagePay FORMS processing).

The flow I'd like to see is the normal flow:
- Cart
- Addresses
- Shipping
- Select Payment Method
- Confirm

But then the flow changes a bit:
--> Confirm calls OrderManager.PlaceOrder
--> Within OrderManager.PlaceOrder, the method PaymentManager.ProcessPayment(paymentInfo, customer, OrderGuid, ref processPaymentResult); is called, which in turn calls ProcessPayment on the IPaymentMethod
--> Within my ProcessPayment, I'd like to redirect the user to a remote posted form.
--> Note that because of the redirect, the Shopping Cart is not cleared and the Order is not saved to the database
--> The user interacts with the payment service to confirm the payment.
--> This remote processing returns eventually to a page similar to WorldPayReturnPage.aspx - possibly with success, possibly with failure.
--> This page can then process the return parameters, and then (if the payment was successful) call OrderManager.PlaceOrder passing in the OrderGuid and the ShoppingCart or (on failure) it can display an error message and give the user a chance to (for example) try a different card.

I believe it would be possible (and fairly straightforward) to make the return page (WorldPayReturnPage.aspx) generic - it could use an interface to process whatever the return parameters are from the individual payment service.

I have actually implemented this flow for one project (will announce the URL when it goes live) and it seems to work OK - certainly it's nice for the user that they don't lose their shopping cart if, for whatever reason, their card payment is declined.

Does this make any sense to anyone? Maybe I should draw a picture of the flow?

Stuart
10 years ago
Hi

I am looking at the similar solution. Is it possible for you to provide more details or share this portion of the code to call the remote form and capture the return results

Nikesh Shah
10 years ago
As if by coincidence... I was actually emailing a dev at nopcommerce.com about this only yesterday!

The basic flow of what I did was...

1. In OrderManager I changed the way the OrderGuid was created so that a caller could pass in the OrderGuid if required.

        /// <summary>
        /// Places an order
        /// </summary>
        /// <param name="paymentInfo">Payment info</param>
        /// <param name="customer">Customer</param>
        /// <param name="OrderID">Order identifier</param>
        /// <returns>The error status, or String.Empty if no errors</returns>
        public static string PlaceOrder(PaymentInfo paymentInfo, Customer customer, out int OrderID)
        {
            Guid OrderGuid = Guid.NewGuid();
            return PlaceOrder(paymentInfo, customer, OrderGuid, out OrderID);
        }

        /// <summary>
        /// Places an order
        /// </summary>
        /// <param name="paymentInfo">Payment info</param>
        /// <param name="customer">Customer</param>
        /// <param name="OrderGuid">Order GUID to use</param>
        /// <param name="OrderID">Order identifier</param>
        /// <returns>The error status, or String.Empty if no errors</returns>
        public static string PlaceOrder(PaymentInfo paymentInfo, Customer customer, Guid OrderGuid, out int OrderID)
        {
...


2. In my custom payment processor I used the OrderGuid as the "Session Variable identifier" with my remote payment service and then did my posting from within that payment processor

3. In the return from the payment processor, I called a new web page which basically calls the new PlaceOrder method in OrderManager.cs:

    public partial class CheckoutSecureReturn : BaseNopPage
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            Response.CacheControl = "private";
            Response.Expires = 0;
            Response.AddHeader("pragma", "no-cache");
            Response.Cache.SetCacheability(HttpCacheability.NoCache);

            if ((NopContext.Current.User == null) || (NopContext.Current.User.IsGuest && !NopContext.Current.AnonymousCheckoutAllowed))
            {
                Response.Redirect("~/Login.aspx");
            }

            var cart = ShoppingCartManager.GetCurrentShoppingCart(ShoppingCartTypeEnum.ShoppingCart);
            if (cart.Count == 0)
                Response.Redirect("~/ShoppingCart.aspx");


            Guid orderGuid = Guid.Empty;
            try
            {
                string candidateGuid = Request.Params["OId"];
                orderGuid = new Guid(candidateGuid);
            }
            catch (Exception)
            {
                Response.Redirect("~/CheckoutError.aspx?Error=" + Server.UrlEncode(GetLocaleResourceString("CheckoutError.OrderIdInternal")), true);
            }

            try
            {
                PaymentInfo paymentInfo = this.PaymentInfo;
                if (paymentInfo == null)
                    Response.Redirect("~/CheckoutPaymentInfo.aspx");

                // if we have made it here, then we have performed ThreeDSecure
                paymentInfo.ThreeDSecureAuthPerformed = true;

                paymentInfo.BillingAddress = NopContext.Current.User.BillingAddress;
                paymentInfo.ShippingAddress = NopContext.Current.User.ShippingAddress;
                paymentInfo.CustomerLanguage = NopContext.Current.WorkingLanguage;
                paymentInfo.CustomerCurrency = NopContext.Current.WorkingCurrency;

                int orderID = 0;
                string result = OrderManager.PlaceOrder(paymentInfo, NopContext.Current.User, orderGuid, out orderID);
                this.PaymentInfo = null;
                Order order = OrderManager.GetOrderByID(orderID);
                if (!String.IsNullOrEmpty(result))
                {
                    ShowError(result);
                    return;
                }
                else
                    PaymentManager.PostProcessPayment(order);
                Response.Redirect("~/CheckoutCompleted.aspx");
            }
            catch (ThreadAbortException taex)
            {
                // not a problem - indicates a redirect
                throw;
            }
            catch (Exception exc)
            {
                LogManager.InsertLog(LogTypeEnum.OrderError, exc.Message, exc);
                ShowError(exc.Message);
            }
        }

It's not perfect, but it's proven solid and reliable in use so far (touch wood)
10 years ago
I understand that this is for 3DSecure payment.
Can you please email the modules

Thank you
10 years ago
I understand this is post type where you call another website with parameters ? If so this is fine for one mode of operation where you dont record the order if the payment is not successful.

But what happens if the post does not return to the worldpaymentreturn. There is no guarantee that it will and the user may have allready paid via the remote service and then there is an error of some description and it does not return and you won't know what the payment you recieve is for ? Am I understanding you correctly ?

With the current logic at least you have an unpaid order to match the payment with.

Certainly some sort of better integration is desirable but you need a setting to switch to your mode of operation ?
10 years ago
Yidna wrote:
I understand this is post type where you call another website with parameters ? If so this is fine for one mode of operation where you dont record the order if the payment is not successful.

But what happens if the post does not return to the worldpaymentreturn. There is no guarantee that it will and the user may have allready paid via the remote service and then there is an error of some description and it does not return and you won't know what the payment you recieve is for ? Am I understanding you correctly ?

With the current logic at least you have an unpaid order to match the payment with.

Certainly some sort of better integration is desirable but you need a setting to switch to your mode of operation ?


In our final implementation on this, we captured the order details in the log file before the external call.

In the first month of running the shop, we had no issues with the flow not returning to worldpaymentreturn - but had hundreds of cases where the external payment (3D Secure) failed - and in approx 50% of them the user came back tried again with a different card - that would not have happened if we hadn't changed the flow.
10 years ago
hgmamaci wrote:
I understand that this is for 3DSecure payment.
Can you please email the modules

Thank you


I contributed the Sage implementation back to nopCommerce (for the pro release?) plus it's on CodePlex somewhere. the other implementation (different payment provider) is not public as it was paid for by a customer - sorry.
10 years ago
I saw it. Thank you. That was really helpful
10 years ago
Interesting implementation for sure, I have thought about this myself and I have come to the conclusions that there are pros and cons with everything, here's my though on the matter.

Today we run a zen cart based shop (which will change shortly) but we have seen issues with orders being paid for but without any order placed, this happens when the user doesn't click the final button when they pay with credit card, this has only happened twice but it's a real pain when it happens, so with this in mind I had another way of implementing the solution in nop, here's how I did it.

The order is placed in pending state and marked as unpaid when the confirm button is pressed.
The used is redirected to the site where the payment will take place and then:

If successful, the success page is called and the order is marked as paid.
If failed, the cancel page is called and the order is cancelled at the same time I call re-order and send the user to the shoppingcart, which means that the user will see all his items in the basket just as it was before.

With this solution, if the user for some reason is not returned back correctly, we can easily mark the order as paid manually, in this way we don't loose any orders that are in fact paid for.

We have however a small issue that one of our payment services don't call cancel with any parameters at all, which is really a poor implementation, so here we cannot re-order autmoatically, but the user is informed about this when they return how to do this, I haven't solved this issue yet :(

Please comments whatever flaws that may lie in this implementation since I'm new to nop :)

BR
Joakim