Sharing: How to handle payment method options in commerce project that base on asp.net mvc.

Vote:
 

I have been requested for a blog to share our journey on working on a EPIServer 7.5 Commerce project, but while waiting for approval to create one. I am just gonna share the experiences on forum here , hopefully it could help others, and also for us to learn by your comments and feedback.

According to the webform sample from EPiServer Commerce, each payment method has a paymenthod.ascx file that will handle the payment form, validation and process to add to cart order form. These will be loaded on the payment page check out.

However, it is totally different approach when working with MVC and we have found the following solution for it:

General idea is:

  1. Each payment method will have its own controller and action to return its own form. Controller name will be based on the system keyword of the payment method, so that it is unique.
  2. On payment page, all payment options available will be loaded into a dropdown list. When user select the option from dropdown list, we will load the payment form accordingly base on the payment method system name.
  3. When the form is submitted back to the action, the data will be converted to IPaymentOption, so that we could get the payment for all types of payments.

Here is how the problem was tackled:

First we need few model classes for current payment types (hold the actual payment details within the payment method):

 [Serializable]

    public abstract class PaymentModel

    {

        public Guid PaymentMethodId { get; set; }

        public string PaymentMethodSystemName { get; set; }

        public int OrderFormId { get; set; }

        public int OrderGroupId { get; set; }

        public virtual string Type { get; set; }

    }

 [Serializable]

    public class OtherPaymentModel : PaymentModel

    {

        public override string Type

        {

get { return "Niteco.Web.App.Areas.Commerce.Models.PaymentMethods.OtherPaymentModel," + Assembly.GetExecutingAssembly(); }

        }

    }

 [Serializable]

    public class CreditCardPaymentModel:PaymentModel

    {

        public string CardType { get; set; }

       

        [RequiredIf("RequireCardName",true)]

        public string CreditCardName { get; set; }

 

        [Required]

        [CreditCard]

        public string CreditCardNumber { get; set; }

 

              //More within the zip code

        }

    }

For each payment method, we will be creating 2 of the following classes for each: paymentmethodmodel, and paymentmetoption, .i.e:

  public class GenericPaymentMethodModel : PaymentMethodModel

    {

        public OtherPaymentModel Payment { get; set; }

    }

 

  public class GenericPaymentOption : IPaymentOption

    {

        public bool ValidateData()

        {

            return true;

        }

        public OtherPaymentModel Payment { get; set; }

        public GenericPaymentOption(OtherPaymentModel model)

        {

            Payment = model;

        }

        public Payment PreProcess(OrderForm form)

        {

            var otherPayment=new OtherPayment {BillingAddressId = form.BillingAddressId};

            return otherPayment;

        }

 

        //More within the zip code

    }

 

For other method that use CreditCard, we will need to replace OtherPaymentModel with CreditCardPaymentModel

Now we will need to create the controller for each payment method, for example:

 

public class AuthorizePaymentMethodController : Controller

    {

        //

        // GET: /Commerce/Authorize/

        [RestoreModelStateFromTempData]

        public ActionResult PaymentMethod(Guid paymentMethodId, string paymentMethodSystemName, CreditCardPaymentModel paymentModel )

        {

            var model = new AuthorizePaymentMethodModel

            {

Payment = paymentModel ?? new CreditCardPaymentModel() { ExpirationMonth = DateTime.Now.Month, ExpirationYear = DateTime.Now.Year }

            };

              //More within the zip code

           

            model.Payment.PaymentMethodSystemName = paymentMethodSystemName;

            return PartialView("AuthorizePaymentMethod", model);

        }

    }

 

The view for it will look like:

 

@model Niteco.Web.App.Areas.Commerce.Models.PaymentMethods.AuthorizePaymentMethodModel

<h5>@Model.PaymentMethodFriendlyName</h5>

<br/>

@Html.HiddenFor(x => x.Payment.PaymentMethodSystemName)

@Html.HiddenFor(x => x.Payment.PaymentMethodId)

@Html.HiddenFor(x => x.Payment.Type)

@*More within the zip code *@

 

The same apply for all other payment methods, need to make sure the controller name is made up of {PaymentMethodSystemKeyword} + “PaymentMethodController”. 

Then we will need a model class for holding the payment method information

 

public class PaymentMethodModel

    {

        public Guid PaymentMethodId { get; set; }

        public string PaymentMethodSystemName { get; set; }

        public string PaymentMethodFriendlyName { get; set; }

        public string PaymentDescription { get; set; }

        public MarketId MarketId { get; set; }

    }

 

And here is the model class to hold the payment data

public class PaymentFormModel

    {

        public List<PaymentMethodModel> PaymentMethods { get; set; }

        [Required]

        public string SelectedPaymentMethod { get; set; }

        public PaymentModel Payment { get; set; }

    }

 

And when return the model for the form, you will need to return the form you will need to create a new instance for it,.i.e:

 

return View(new PaymentPageModel(currentPage) { PaymentFormModel = new PaymentFormModel (){ PaymentMethods =PaymentHelper.GetPaymentMethods() }});

 

The View will be generated like this:

  @using (Ajax.BeginForm("Pay", "Checkout", new AjaxOptions() { UpdateTargetId = "paymentMethodContainer" }))

    {

        <div class="checkout_block_title">

            <h4>

                @Html.Translate("/PaymentMethods/paymentoptiontitle")

            </h4>

        </div>

 

        <div id="paymentMethodContainer">

            @{ Html.RenderPartial("PaymentForm", Model. PaymentFormModel); }

        </div>

 

        <input type="submit" class="btn_02" value="Submit" />

    }

The partial view “PaymentForm “ will be like this:

<div class="control-group">

    @Html.LabelFor(x => x.SelectedPaymentMethod, Html.Translate("/PaymentMethods/SelectPaymentMethodTitle"), new { @class = "control-label" })

    <span class="important">*</span>

    <div class="controls">

        @Html.DropDownList("SelectedPaymentMethod",

    new SelectList(Model.PaymentMethods, "PaymentMethodId", "PaymentMethodSystemName",

        Model.SelectedPaymentMethod), Html.Translate("/Paymentmethods/selectpaymentmethodlabel"), new { @class = "select_01" })       

        <div class="error">@Html.ValidationMessageFor(x => x.SelectedPaymentMethod)</div>

        <br />

    </div>

</div>

<div id="selectedPaymentContainer">

        <div class="error">

            @if (!ViewData.ModelState.IsValid)

            {

                <h5>@Html.Translate("/Paymentmethods/PaymentErrorSummary")</h5>

                @Html.ValidationSummary()

            }

        </div>

 

        @if (Model.Payment != null)

        {

            Html.RenderAction("PaymentMethod", Model.Payment.PaymentMethodSystemName + "PaymentMethod", new RouteValueDictionary(new

            {

                paymentMethodId = Model.Payment.PaymentMethodId,

                paymentMethodSystemName = Model.Payment.PaymentMethodSystemName,

                paymentModel = Model.Payment

            }));

        }

 

</div>

<script type="text/javascript" language="javascript">

 

    $('select[name="SelectedPaymentMethod"]').change(function () {

        var selectedMethodId = $(this).val();

        var selectedMethodName = $('select[name="SelectedPaymentMethod"] option:selected').text();

 

        var href = "/" + selectedMethodName + "PaymentMethod/PaymentMethod";

        $.ajax(

           {

               url: href,

               data: { paymentMethodId: selectedMethodId, paymentMethodSystemName: selectedMethodName },

               success: function (data) {

                   $("#selectedPaymentContainer").html(data);

               }

           });

 

    });

</script>

 

Now on the submit action we will be able to get the payment details to be added to cart:

 

  public ActionResult Signup(ClubSignupFormModel model)

        {

            model.PaymentMethods = PaymentHelper.GetPaymentMethods();

            if (!ModelState.IsValid) return PartialView("Signup",model);  

            //foreach (var paymentMethod in model.PaymentMethods.Values)

            if (model.Payment!=null)

            {

                var paymentMethod = model.Payment;

               

               

                var paymentMethodType = Type.GetType("Niteco.Web.App.Areas.Commerce.Models.PaymentMethods." + paymentMethod.PaymentMethodSystemName + "PaymentOption," + Assembly.GetExecutingAssembly());

 

                if (paymentMethodType != null)

                {

                    var paymentOptionType = Activator.CreateInstance(paymentMethodType, paymentMethod);

                    var paymentOption = paymentOptionType as IPaymentOption;

 

                    //This is to handle payment if needed, for example

                    if (paymentOption != null)

                    {

                        //Add payment to cart here

                        //return Content("Payment added to cart");

                    }

                }

 

                ModelState.AddModelError("NoPaymentSelected", LocalizationService.Current.GetString("/PaymentMethods/UnableToProcessPayment"));

                return PartialView("Signup",model);  

            }

            ModelState.AddModelError("NoPaymentSelected", LocalizationService.Current.GetString("/PaymentMethods/NoPaymentSelectedError"));

            return PartialView("SignUp",model);

        }

 

One more thing that we need to handle is the model binding for our abstract payment model class, this will be needed:

public class CommerceModelBinder:DefaultModelBinder

    {

        protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)

        {

            if (modelType != typeof (PaymentModel))

                return base.CreateModel(controllerContext, bindingContext, modelType);

            var instantiationTypeString = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Type").ConvertTo(typeof(string)).ToString();

            var instantiationType = Type.GetType(instantiationTypeString);

            if (instantiationType == null) return base.CreateModel(controllerContext, bindingContext, modelType);

            var obj = Activator.CreateInstance(instantiationType);

            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType);

            bindingContext.ModelMetadata.Model = obj;

            return obj;

        }

 

    }

This class is very important for the payment data to be binding correctly when form is submitted. This will need to be registered on global asax on Application Start:

ModelBinders.Binders.DefaultBinder=new CommerceModelBinder();

 

That is it, here is a screencast of how the payment options will work like in our MVC EPiServer Commerce Project:

http://www.screencast.com/t/RwBMip2jc

I will zip up the code and attach to this thread later on.

#81022
Feb 07, 2014 10:39
Vote:
 

Hi Tuon


Thank you very much for you post. This is just what I was looking for.


I look forward to seing some code from you.


Regards
Anders

#81098
Feb 10, 2014 11:48
Vote:
 

hi Anders,

 

I'm glad that it helps, here is the link to the code :https://www.dropbox.com/s/wfym093vfak7eg2/EPIServer_Comerce_7.5_PaymentMethods_Sample.zip

 

Thanks

#81241
Feb 12, 2014 11:52
Vote:
 

Thank you for sharing your code and your thoughts.

So far, I think that some of your solutions fit very well with our site.

Regards

Anders

#81242
Feb 12, 2014 11:59
Vote:
 

HI Anders,

That is really good. We are in the process of upgrading one of our customer from EPiServer Commerce 6 R2 to 7.5, and there is a lot of new things that we want to share with the rest of EPiServer World, like how to drag and drop catalog node and variant on to content area with different display options, using of html5 for pages history and stuff, but my request to create blog seem got lost somewhere, and I couldn't see the syndycation function on the website .

#81263
Feb 13, 2014 3:40
Vote:
 

Hi Tuon


That sounds like some interresting scenarios, that I would like to hear more about.

I can't help you with the Blog request, but perhaps you should try to request it again?

Regards

Anders

#81267
Feb 13, 2014 7:16
Vote:
 

hi Andres,

Thanks, I have created my blog now, you could see it from episerver world, will keep the posts coming regularly.

#81398
Feb 17, 2014 7:46
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.