[SerilizableCart] Migrating IPaymentGateway using abstraction - IPaymentPlugin
As you know, the SerializableCart was released in Episerver Commerce 10.2.0. This improves performance a lot. However, before using SerializableCart, we need to first complete some tasks.
This post describes how to "Convert IPaymentGateway to IPaymentPlugin using abstraction classes" - the most important task that needs to be done when enabling SerializableCart mode.
SerializableCart only supports abstraction and no-workflow. So our Payment providers also need to use abstraction or ... you will break your payment :).
Environment
- Visual Studio 2013 or later version for our implementation.
- This guide uses open source payment provider from Episerver - DIBS payment provider.
We could do the same with other payment providers.
Upgrading payment provider project.
- Open the payment provider project.
- In the Package Manager Console, install the latest Episerver.Commerce.Core package (at least 10.2.3)
Migrating payment gateway.
Making our payment gateways implement the IPaymentPlugin interface
public bool ProcessPayment(IPayment payment, ref string message) { throw new NotImplementedException(); }
Adding IOrderGroup property
public IOrderGroup OrderGroup {get; set;}
Why we do this
The EPiServer.Commerce.Order.IPayment interface is quite different from Mediachase.Commerce.Orders.Payment. The IPayment interface doesn't have Parent.Parent or Parent methods like Payment, so we have to add IOrderGroup to our payment provider property - IOrderGroup will be used instead of Payment.Parent.Parent.
Change old implementation method
Move all code in ProcessPayment(Payment payment, ref string message) to the new method ProcessPayment(IPayment payment, ref string message) and add the following code:
private IOrderForm _orderForm; public override bool ProcessPayment(Mediachase.Commerce.Orders.Payment payment, ref string message) { OrderGroup = payment.Parent.Parent; _orderForm = payment.Parent; return ProcessPayment(payment as IPayment, ref message); }
Why we do this
Those lines of code support compatibility with workflow payment process.
And ProcessPayment(IPayment payment, ref string message) method should be like this:
public bool ProcessPayment(IPayment payment, ref string message) { if (HttpContext.Current != null) { if (payment.Parent.Parent is PurchaseOrder) { if (payment.TransactionType == TransactionType.Capture.ToString()) { //return true meaning the capture request is done, //actual capturing must be done on DIBS. string result = PostCaptureRequest(payment); //result containing ACCEPTED means the the request was successful if (result.IndexOf("ACCEPTED") == -1) { message = "There was an error while capturing payment with DIBS"; return false; } return true; } if (payment.TransactionType == TransactionType.Credit.ToString()) { var transactionID = payment.TransactionID; if (string.IsNullOrEmpty(transactionID) || transactionID.Equals("0")) { message = "TransactionID is not valid or the current payment method does not support this order type."; return false; } //The transact must be captured before refunding string result = PostRefundRequest(payment); if (result.IndexOf("ACCEPTED") == -1) { message = "There was an error while refunding with DIBS"; return false; } return true; } //right now we do not support processing the order which is created by Commerce Manager message = "The current payment method does not support this order type."; return false; } Cart cart = payment.Parent.Parent as Cart; if (cart != null && cart.Status == PaymentCompleted) { //return true because this shopping cart has been paid already on //DIBS return true; } var pageRef = DataFactory.Instance.GetPage(PageReference.StartPage)["DIBSPaymentPage"] as PageReference; PageData page = DataFactory.Instance.GetPage(pageRef); HttpContext.Current.Response.Redirect(page.LinkURL); } return true; }
Migrating new method
In ProcessPayment(IPayment payment, ref string message) and related methods, we have to use abstraction instead Legacy Cart.
We have some "rules" for our migration:
Old way | Abstraction |
---|---|
Mediachase.Commerce.Orders.Payment | EPiServer.Commerce.Order.IPayment |
Payment.Parent | _orderForm |
Payment.Parent.Parent | _orderGroup |
payment.Parent.Parent.BillingCurrency | _orderGroup.Currency |
Cart | ICart |
Cart.Id | ICart.OrderLink.OrderGroupId |
CartHelper | (removed) - use ICart (_orderGroup) instead |
CartHelper(Cart.DefaultName).Cart | IOrderRepository.LoadCart(CustomerContext.Current.CurrentContactId, Cart.DefaultName) |
cartHelper.PrimaryAddress | payment.BillingAddress |
cartHelper.FindAddressByName(payment.Parent.BillingAddressId); Billing Address | payment.BillingAddress |
cartHelper.FindAddressByName(payment.Parent.Shipments[0].ShippingAddressId) Shipping Address | _orderGroup.GetFirstShipment().ShippingAddress |
Cart.LineItems or payment.Parent.LineItems | _orderForm.GetAllLineItems() |
LineItem.ExtendedPrice | ILineItem.GetExtendedPrice() |
OrderAddress.AcceptChanges() | (removed) |
Payment.AcceptChanges() | (removed) |
Cart.AcceptChanges() or PurchaseOrder.AcceptChanges() | (removed) - use IOrderRepository.Save() instead |
Cart.OrderForms[0].Payments | ICart.GetFirstForm().Payments |
payment.Parent.TaxTotal or OrderForm.TaxTotal | (removed) - use ITaxCalculator instead |
Cart.Total or PurchaseOrder.Total | use IOrderGroup.GetTotal() instead |
After migration, our ProcessPayment(IPayment payment, ref string message) method will be like this:
public bool ProcessPayment(IPayment payment, ref string message) { if (HttpContext.Current == null) { return true; } if (OrderGroup is PurchaseOrder) { if (payment.TransactionType == TransactionType.Capture.ToString()) { //return true meaning the capture request is done, //actual capturing must be done on DIBS. string result = PostCaptureRequest(payment); //result containing ACCEPTED means the request was successful if (result.IndexOf("ACCEPTED") == -1) { message = "There was an error while capturing payment with DIBS"; return false; } return true; } if (payment.TransactionType == TransactionType.Credit.ToString()) { var transactionID = payment.TransactionID; if (string.IsNullOrEmpty(transactionID) || transactionID.Equals("0")) { message = "TransactionID is not valid or the current payment method does not support this order type."; return false; } //The transact must be captured before refunding string result = PostRefundRequest(payment); if (result.IndexOf("ACCEPTED") == -1) { message = "There was an error while refunding with DIBS"; return false; } return true; } //right now we do not support processing the order which is created by Commerce Manager message = "The current payment method does not support this order type."; return false; } var cart = OrderGroup as ICart; if (cart != null && cart.OrderStatus.ToString() == PaymentCompleted) { // return true because this shopping cart has been paid already on DIBS return true; } _orderRepository.Service.Save(OrderGroup); var pageRef = DataFactory.Instance.GetPage(PageReference.StartPage)["DIBSPaymentPage"] as PageReference; PageData page = DataFactory.Instance.GetPage(pageRef); HttpContext.Current.Response.Redirect(page.LinkURL); return false; }
public class DIBSPaymentGateway : AbstractPaymentGateway, IPaymentPlugin { public const string UserParameter = "MerchantID"; public const string PasswordParameter = "Password"; public const string ProcessingUrl = "ProcessingUrl"; public const string MD5Key1 = "MD5Key1"; public const string MD5Key2 = "MD5Key2"; public const string PaymentCompleted = "DIBS payment completed"; private string _merchant; private string _password; private PaymentMethodDto _payment; private readonly Injected<IOrderRepository> _orderRepository; private IOrderForm _orderForm; public IOrderGroup OrderGroup { get; set; } public override bool ProcessPayment(Mediachase.Commerce.Orders.Payment payment, ref string message) { OrderGroup = payment.Parent.Parent; _orderForm = OrderGroup.Forms.FirstOrDefault(form => form.Payments.Contains(payment)); return ProcessPayment(payment as IPayment, ref message); } public bool ProcessPayment(IPayment payment, ref string message) { if (HttpContext.Current == null) { return true; } if (OrderGroup is PurchaseOrder) { if (payment.TransactionType == TransactionType.Capture.ToString()) { //return true meaning the capture request is done, //actual capturing must be done on DIBS. string result = PostCaptureRequest(payment); //result containing ACCEPTED means the request was successful if (result.IndexOf("ACCEPTED") == -1) { message = "There was an error while capturing payment with DIBS"; return false; } return true; } if (payment.TransactionType == TransactionType.Credit.ToString()) { var transactionID = payment.TransactionID; if (string.IsNullOrEmpty(transactionID) || transactionID.Equals("0")) { message = "TransactionID is not valid or the current payment method does not support this order type."; return false; } //The transact must be captured before refunding string result = PostRefundRequest(payment); if (result.IndexOf("ACCEPTED") == -1) { message = "There was an error while refunding with DIBS"; return false; } return true; } //right now we do not support processing the order which is created by Commerce Manager message = "The current payment method does not support this order type."; return false; } var cart = OrderGroup as ICart; if (cart != null && cart.OrderStatus.ToString() == PaymentCompleted) { // return true because this shopping cart has been paid already on DIBS return true; } _orderRepository.Service.Save(OrderGroup); var pageRef = DataFactory.Instance.GetPage(PageReference.StartPage)["DIBSPaymentPage"] as PageReference; PageData page = DataFactory.Instance.GetPage(pageRef); HttpContext.Current.Response.Redirect(page.LinkURL); return false; } /// <summary> /// Posts the request to DIBS API. /// </summary> /// <param name="payment">The payment.</param> /// <param name="url">The URL.</param> /// <returns>A string contains result from DIBS API</returns> private string PostRequest(IPayment payment, string url) { WebClient webClient = new WebClient(); NameValueCollection request = new NameValueCollection(); var po = OrderGroup as PurchaseOrder; string orderid = po.TrackingNumber; string transact = payment.TransactionID; string currencyCode = OrderGroup.Currency; string amount = Utilities.GetAmount(new Currency(currencyCode), payment.Amount); request.Add("merchant", Merchant); request.Add("transact", transact); request.Add("amount", amount); request.Add("currency", currencyCode); request.Add("orderId", orderid); string md5 = GetMD5KeyRefund(Merchant, orderid, transact, amount); request.Add("md5key", md5); // in order to support split payment, let's set supportSplitPayment to true, and make sure you have enabled Split payment for your account // http://tech.dibspayment.com/flexwin_api_other_features_split_payment var supportSplitPayment = false; if (supportSplitPayment) { request.Add("splitpay", "true"); } else { request.Add("force", "yes"); } request.Add("textreply", "yes"); webClient.Credentials = new NetworkCredential(Merchant, Password); byte[] responseArray = webClient.UploadValues(url, "POST", request); return Encoding.ASCII.GetString(responseArray); } /// <summary> /// Posts the capture request to DIBS API. /// </summary> /// <param name="payment">The payment.</param> /// <returns>Return string from DIBS API</returns> private string PostCaptureRequest(IPayment payment) { return PostRequest(payment, "https://payment.architrade.com/cgi-bin/capture.cgi"); } /// <summary> /// Posts the refund request to DIBS API. /// </summary> /// <param name="payment">The payment.</param> private string PostRefundRequest(IPayment payment) { return PostRequest(payment, "https://payment.architrade.com/cgi-adm/refund.cgi"); } /// <summary> /// Gets the payment. /// </summary> /// <value>The payment.</value> public PaymentMethodDto Payment { get { if (_payment == null) { _payment = PaymentManager.GetPaymentMethodBySystemName("DIBS", SiteContext.Current.LanguageName); } return _payment; } } /// <summary> /// Gets the merchant. /// </summary> /// <value>The merchant.</value> public string Merchant { get { if (string.IsNullOrEmpty(_merchant)) { _merchant = GetParameterByName(Payment, DIBSPaymentGateway.UserParameter).Value; } return _merchant; } } /// <summary> /// Gets the password. /// </summary> /// <value>The password.</value> public string Password { get { if (string.IsNullOrEmpty(_password)) { _password = GetParameterByName(Payment, DIBSPaymentGateway.PasswordParameter).Value; } return _password; } } /// <summary> /// Gets the M d5 key refund. /// </summary> /// <param name="merchant">The merchant.</param> /// <param name="orderId">The order id.</param> /// <param name="transact">The transact.</param> /// <param name="amount">The amount.</param> /// <returns></returns> public static string GetMD5KeyRefund(string merchant, string orderId, string transact, string amount) { string hashString = string.Format("merchant={0}&orderid={1}&transact={2}&amount={3}", merchant, orderId, transact, amount); return GetMD5Key(hashString); } /// <summary> /// Gets the MD5 key used to send to DIBS in authorization step. /// </summary> /// <param name="merchant">The merchant.</param> /// <param name="orderId">The order id.</param> /// <param name="currency">The currency.</param> /// <param name="amount">The amount.</param> /// <returns></returns> public static string GetMD5Key(string merchant, string orderId, Currency currency, string amount) { string hashString = string.Format("merchant={0}&orderid={1}¤cy={2}&amount={3}", merchant, orderId, currency.CurrencyCode, amount); return GetMD5Key(hashString); } /// <summary> /// Gets the key used to verify response from DIBS when payment is approved. /// </summary> /// <param name="transact">The transact.</param> /// <param name="amount">The amount.</param> /// <param name="currency">The currency.</param> /// <returns></returns> public static string GetMD5Key(string transact, string amount, Currency currency) { string hashString = string.Format("transact={0}&amount={1}¤cy={2}", transact, amount, Utilities.GetCurrencyCode(currency)); return GetMD5Key(hashString); } private static string GetMD5Key(string hashString) { PaymentMethodDto dibs = PaymentManager.GetPaymentMethodBySystemName("DIBS", SiteContext.Current.LanguageName); string key1 = GetParameterByName(dibs, MD5Key1).Value; string key2 = GetParameterByName(dibs, MD5Key2).Value; MD5CryptoServiceProvider x = new MD5CryptoServiceProvider(); byte[] bs = System.Text.Encoding.UTF8.GetBytes(key1 + hashString); bs = x.ComputeHash(bs); StringBuilder s = new StringBuilder(); foreach (byte b in bs) { s.Append(b.ToString("x2").ToLower()); } string firstHash = s.ToString(); string secondHashString = key2 + firstHash; byte[] bs2 = System.Text.Encoding.UTF8.GetBytes(secondHashString); bs2 = x.ComputeHash(bs2); StringBuilder s2 = new StringBuilder(); foreach (byte b in bs2) { s2.Append(b.ToString("x2").ToLower()); } string secondHash = s2.ToString(); return secondHash; } /// <summary> /// Gets the parameter by name. /// </summary> /// <param name="paymentMethodDto">The payment method dto.</param> /// <param name="name">The name.</param> /// <returns></returns> internal static PaymentMethodDto.PaymentMethodParameterRow GetParameterByName(PaymentMethodDto paymentMethodDto, string name) { PaymentMethodDto.PaymentMethodParameterRow[] rowArray = (PaymentMethodDto.PaymentMethodParameterRow[])paymentMethodDto.PaymentMethodParameter.Select(string.Format("Parameter = '{0}'", name)); if ((rowArray != null) && (rowArray.Length > 0)) { return rowArray[0]; } throw new ArgumentNullException("Parameter named " + name + " for DIBS payment cannot be null"); } }
Finally, we can rebuild the project, then follow the steps in this guide to deploy to our development site and verify payment with SerializableCart.
The code in this blog post is available in my GitHub as well - you could download it.
Hope this helps.
/Son Do
Comments