Monday, October 22, 2012

The making of DevReach.com - Part 4, Integration with PayPal Checkout Express

This is going to be a series of blog posts, that will review the process of making the latest version of DevReach.Com. For the this version we are going to use Sitefinity latest version and exploit all of its features as much as we can. We are currently working together with Sitefinity Team and producing a lot of feedback for them, part of the features that we need will probably be part of the next release of the product.

In the previous post from this series, I stopped at the final step of DevReach's process - choosing the payment method. I showed you how to place an offline order and promised to show you how we integrated our eCommerce module with PayPal Express Checkout.

First a few words what actually is PayPal Express Checkout and why we decided to take this approach. Sitefinity eCommerce module gives you out of the box integration with paypal using PayPal Payflow Pro. With this approach all the billing details (credit card details) are entered on your site and sitefinity process the payment using paypal API. On the other hand, when using PayPal Express Checkout - the users use their paypal account and all the sensitive data are entered on PayPal. We believe that this way our customer will be more comfortable, although we should probably consider pure credit card payments (as we get many requests this year).

Before starting

In order to start the steps bellow, you need to have a developer paypal account created (and a real one once you decide to go live). Creating dev account on paypal is easy: just go to developer.paypal.com and register. After that you will be able to create all kind of test accounts (both business and personal ones) that will be available in your paypal sandbox. You need at least on business test account in order to use its parameters for sending test payments to PayPal.

How PayPal Express Checkout works?

When the user selects the paypal method and click on the Place Order button, after saving all the information for the order, we navigate the user to PayPal page with some parameters that identify the transaction and some details for the transaction iteself (price, products, currency, etc.). Once the user confirms the order on the paypal website, he is navigated back to a certain page of your website and you need to confirm the payment on your side in order to complete the transaction. The response from the transaction is retrieved real time and the order can be automatically marked as paid.

The setup

In order to implement the PayPal Express Checkout widget, you should create a new Payment processor and Payment method. This way all the settings required for the widget will be fully configurable through the sitefinity administration real time. Of course, you can always simplify things and hard code those settings or extract them as a custom configuration in the web.config, but this way changing settings (especially when migrating from test environment to live environment) will be much harder.

Payment processor

First, you need a new class that derives from IPaymentProcessorProvider and implements all public methods of this interface. The implementations could be simply return null or even better - throw exception. These methods shouldn't be actually called as we are going to process the transaction in a bit different way that this processors can't handle. Then you should register you new payment processor in the Sitefinity administration (the actual types may vary according to your implementation):

You will also need a settings class that defines that layout of your paypal settings that will be shown in the backend. Of course, you can arrange it as you wish, but here is an example provided by eCommerce Team in Sitefinity that we used:
In sitefinity backend go to Administration -> Settings -> Advanced -> PaymentProcessor -> PaymentProcessorProviders

Click on the "Create New" button.
Set Id = f1aab7d9-71e4-4e4c-9359-7f78248147e5
Set Name = "PayPalExpress"
Set Title = "PayPal Express"
Set SettingsType = "Telerik.Sitefinity.Samples.Ecommerce.PayPalExpress.PayPalExpressSettings"
Set ViewProviderType = "Telerik.Sitefinity.Samples.Ecommerce.PayPalExpress.PayPalExpressSettingsField"
Set ProviderType = "Telerik.Sitefinity.Samples.Ecommerce.PayPalExpress.PayPalExpressProvider"

Payment method

After you have a payment processor ready, you have to register your new payment method (provided that you already have a paypal account, at least in the paypal sandbox). 

Go to Ecommerce -> Payment Methods
Click on Create a payment method
Set Name = "PayPal Express Checkout"
Select Type = "Online payment"
Select Payment processor = "PayPal Express"
Leave InvoiceUrl blank
Set CancelUrl to the name of your shopping cart page (do not end with ".aspx", just use the NAME of the page)
Set ReturnUrl to the name of your PayPal Review Order page
Set HostUrl to "https://www.sandbox.paypal.com"
Set ApiUrl to "https://api-3t.sandbox.paypal.com/nvp"
Set ApiUsername to "<your paypal test business account>"
Set ApiPassword to "<your paypal test password>"
Set ApiSignature to  "<your paypal test signature>"
Set ApiEnvironment to "sandbox"

And you are good to go with the further implementation. The standard approach would be to create a single widget that handles all PayPal express checkout interaction using multiple views. Unfortunately, our wireframes didn't allow us to do the same and we had to split some things across multiple pages and that's why we have to separate widgets.

In the last post, I stopped on the choose payment method screen and showed you how to place an offline order in the eCommerce module. Now, we will start where we ended last time and will continue with the placing of the online orders. Currently, we have only one online payment type (that is PayPal):


private bool PlacePayPalExpressOrder(PaymentMethod paymentMethod)
{
    PayPalExpressSettings paypalExpressSettings = PayPalExpressHelper.GetPaymentProcessorSettings(paymentMethod);
    string defaultCurrency = Config.Get<ecommerceconfig>().DefaultCurrency;

    CartOrder cartOrder = ShoppingCartManager.GetShoppingCartForUser();
    string providerName = ordersManager.Provider.Name;
    CheckoutState checkoutState = cartOrder.GetCheckoutState();
    Order order = ShoppingCartHelper.CopyCartToOrder(ordersManager, cartOrder, null, this.paymentPayPalProcessorName, providerName);
    User customerUser = null;
    Customer customer = ShoppingCartHelper.GetCustomerInfoOrCreateOneIfDoesntExsist(UserProfileManager.GetManager(), ordersManager, checkoutState, out customerUser);
    order.Customer = customer;
    ordersManager.SaveChanges();
    SetExpressCheckoutRequestType request = PayPalExpressHelper.CreatePaymentRequest(paypalExpressSettings.ReturnUrl,
        paypalExpressSettings.CancelUrl, order.Id, order.OrderNumber, order.Total, defaultCurrency);

    // Populate the payment record with the order details
    request.SetExpressCheckoutRequestDetails.PaymentDetails = new PaymentDetailsType[1];
    request.SetExpressCheckoutRequestDetails.PaymentDetails[0] = PayPalExpressHelper.CreatePaymentDetail(
        order, PayPalExpressHelper.CurrencyCodeStringToType(defaultCurrency));

    // Submit the initial PayPal Express checkout request
    SetExpressCheckoutResponseType response = PayPalExpressHelper.Checkout(paypalExpressSettings, request);

    if (response.Ack == AckCodeType.Success || response.Ack == AckCodeType.SuccessWithWarning)
    {
        string token = response.Token;
        string host = paypalExpressSettings.HostUrl; // "www.sandbox.paypal.com"; 

        string redirectUrl = String.Format("{0}/cgi-bin/webscr?cmd=_express-checkout&token={1}", host, token);

        ShoppingCartManager.IncermentCartOrderStep();
        HttpContext.Current.Response.Redirect(redirectUrl, true);
        return true;
    }

    DisplayMessage(PayPalExpressHelper.GetResponseErrors(response.Errors));

    return false;
}
Basically what we do is to retrieve the settings from the backend, create and actual order in the eCommerce (and create a new Customer of such doesn't exist) and create and send payment request to PayPal. PayPal returns to us token (if everything with the request is ok) and we should redirect the user to paypal providing the token we had acquired.

public static SetExpressCheckoutRequestType CreatePaymentRequest(string returnUrl, string cancelUrl, Guid orderId,
                                                                    int orderNumber, decimal orderTotal, string currency)
{

    SetExpressCheckoutRequestType request = new SetExpressCheckoutRequestType();
    request.SetExpressCheckoutRequestDetails = new SetExpressCheckoutRequestDetailsType();
    request.SetExpressCheckoutRequestDetails.CancelURL = cancelUrl;
    request.SetExpressCheckoutRequestDetails.ReturnURL = returnUrl;
    request.Version = "89.0";

    // ----- version 53.0 and above comment out this section
    request.SetExpressCheckoutRequestDetails.PaymentAction = PaymentActionCodeType.Authorization;
    request.SetExpressCheckoutRequestDetails.InvoiceID = orderNumber.ToString();
    request.SetExpressCheckoutRequestDetails.Custom =  orderId.ToString();
    request.SetExpressCheckoutRequestDetails.OrderDescription = "Order number " + orderNumber.ToString();
    request.SetExpressCheckoutRequestDetails.OrderTotal = new BasicAmountType()
    {
        Value = orderTotal.ToString("0.00"),
        currencyID = PayPalExpressHelper.CurrencyCodeStringToType(currency)
    };
    request.SetExpressCheckoutRequestDetails.GiftWrapAmount = new BasicAmountType()
    {
        Value = 0.ToString("0.00"),
        currencyID = PayPalExpressHelper.CurrencyCodeStringToType(currency)
    };
    // -----

    return request;
}
public static PaymentDetailsType CreatePaymentDetail(Order order, CurrencyCodeType currencyCode, int index = 0)
{
    decimal taxRate = order.Details.First().TaxRate;
    decimal itemTotal = order.Details.Sum(x => x.Total);

    decimal itemsTotalWithoutDiscounts = itemTotal;
    foreach (OrderDiscount od in order.Discounts)
    {
        if (od.DiscountAmountType == DiscountAmountType.Percent)
        {
            itemTotal = itemTotal - (itemTotal * od.DiscountAmount / 100);
        }
        else
        {
            itemTotal = itemTotal - od.DiscountAmount;
        }
    }

    decimal taxTotal = itemTotal * taxRate / 100;
    PaymentDetailsType paymentDetail = new PaymentDetailsType();
    paymentDetail.ItemTotal = new BasicAmountType() { Value = (itemTotal + taxTotal).ToString("0.00"), currencyID = currencyCode };
    paymentDetail.InsuranceTotal = new BasicAmountType() { Value = 0.ToString("0.00"), currencyID = currencyCode };
    paymentDetail.ShippingTotal = new BasicAmountType() { Value = order.ShippingTotal.ToString("0.00"), currencyID = currencyCode };
    paymentDetail.HandlingTotal = new BasicAmountType() { Value = 0.ToString("0.00"), currencyID = currencyCode };
    paymentDetail.TaxTotal = new BasicAmountType() { Value = 0.ToString("0.00"), currencyID = currencyCode };
    //paymentDetail.OrderTotal = new BasicAmountType() { Value = order.Total.ToString("0.00"), currencyID = currencyCode };
    //paymentDetail.OrderDescription = "Order number " + order.OrderNumber.ToString();
    paymentDetail.PaymentAction = PaymentActionCodeType.Authorization;

    int itemNumber = index + 1;
    int detailNumber = 0;
    paymentDetail.PaymentDetailsItem = new PaymentDetailsItemType[order.Details.Count + order.Discounts.Count];

    for (int i = 0; i < order.Details.Count; i++)
    {
        OrderDetail detail = order.Details[i];
        decimal productTotal = 0;
        if (detail.Quantity > 0)
        { 
            productTotal = detail.Total / detail.Quantity;  // Must send cost of each unit as Item amount
        }
        decimal productTax = productTotal * taxRate / 100;

        paymentDetail.PaymentDetailsItem[detailNumber] =
            new PaymentDetailsItemType()
            {
                Amount = new BasicAmountType() { Value = (productTotal + productTax).ToString("0.00"), currencyID = currencyCode },
                Description = detail.Title,
                Name = detail.Title,
                Number = detail.Sku,
                Quantity = detail.Quantity.ToString(),
                Tax = new BasicAmountType() { Value = 0.ToString("0.00"), currencyID = currencyCode },
                ItemWeight = new MeasureType() { unit = "LBS", Value = detail.Weight }
            };
        ++itemNumber;
        ++detailNumber;
    }

    decimal itemsTotal = itemsTotalWithoutDiscounts * (100 + taxRate) / 100;
    for (int i = 0; i < order.Discounts.Count; i++)
    {
        OrderDiscount discount = order.Discounts[i];
        decimal savingsAmount = -(itemsTotal * discount.DiscountAmount / 100);
        itemsTotal = itemsTotal + savingsAmount;

        paymentDetail.PaymentDetailsItem[detailNumber] =
            new PaymentDetailsItemType()
            {
                Amount = new BasicAmountType() { Value = savingsAmount.ToString("0.00"), currencyID = currencyCode },
                Description = discount.Title,
                Name = discount.Title,
                Quantity = "1"
            };
        ++itemNumber;
        ++detailNumber;
    }

    return paymentDetail;
}

public static SetExpressCheckoutResponseType Checkout(PayPalExpressSettings settings, SetExpressCheckoutRequestType request)
{
    CallerServices caller = new CallerServices();
    IAPIProfile profile = PayPalExpressHelper.CreateApiProfile(settings);
    caller.APIProfile = profile;

    SetExpressCheckoutResponseType response = new SetExpressCheckoutResponseType();
    response = (SetExpressCheckoutResponseType)caller.Call("SetExpressCheckout", request);

    return response;
}
After the user is redirected to paypal, he is supposed to enter his paypal credentials and to authorize the payment. Now, the process is pretty much our of your control. The order is save in the backend with status pending and you are able to check its details. Eventually, you can mark it as paid manually if for some reason we don't receive the payment through the standard workflow. In the most of the cases, after the customer has confirmed the payment, he will be redirected to a give url on your page (that you have provided in the request), where he should once again see an overview of his order and confirm it on your website as well. Have in mind that even though the user has accepted the transaction on paypal.com you still have no indication about this in your account and the user is able to drop the transaction at any time.

When the user is navigated to your website two additional parameters are provided through the request. Using them, you should check in paypal system whether such payment actually exists and if it's not paid already:


private void ReviewOrder()
{
    string payerId = "";
    string token = "";
            
    GetPayPalTokenAndPayerId(ref payerId, ref token);

    if (String.IsNullOrWhiteSpace(payerId) || String.IsNullOrWhiteSpace(token))
    {
        displayMessage("Unable to process order.");
        return;
    }
            
    GetExpressCheckoutDetailsResponseType details = PayPalExpressHelper.GetPaymentDetails(paypalExpressSettings, token);

    if (details.Ack == AckCodeType.Failure || details.Ack == AckCodeType.FailureWithWarning)
    {
        displayMessage(PayPalExpressHelper.GetResponseErrors(details.Errors));
        this.btnConfirmPayment.Visible = false;
    }
    else
    {
        this.btnConfirmPayment.Visible = true;
    }
    //displayOrderDetails(details);

    string orderId = new Guid(details.GetExpressCheckoutDetailsResponseDetails.PaymentDetails[0].Custom).ToString();
    this.btnConfirmPayment.CommandArgument = orderId;
}

private void GetPayPalTokenAndPayerId(ref string payerId, ref string token)
{
    payerId = "";
    token = "";

    if (HttpContext.Current.Request["payerId"] != null)
    {
        payerId = HttpContext.Current.Request["payerId"].ToString();
    }

    if (HttpContext.Current.Request["token"] != null)
    {
        token = HttpContext.Current.Request["token"].ToString();
    }
}
public static GetExpressCheckoutDetailsResponseType GetPaymentDetails(PayPalExpressSettings settings, string token)
{
    CallerServices caller = new CallerServices();
    IAPIProfile profile = CreateApiProfile(settings);
    caller.APIProfile = profile;

    GetExpressCheckoutDetailsRequestType request = new GetExpressCheckoutDetailsRequestType();
    request.Token = token;

    GetExpressCheckoutDetailsResponseType response =
    (GetExpressCheckoutDetailsResponseType)caller.Call("GetExpressCheckoutDetails", request);

    return response;
}
Notice that when initially sending request for payment to PayPal we provided unique id for this transaction in our system - in our case GUID but it could int or other type as well. This way, we have one-to-one relationship between the paypal token and our orders and we are able to retrieve the order from our database and show its details. After the user confirms the transaction we are ready to actually complete it:
void btnConfirmPayment_Command(object sender, System.Web.UI.WebControls.CommandEventArgs e)
{
    Guid orderId = new Guid(e.CommandArgument.ToString());
    if (ConfirmOrder(orderId))
    {
        EmailHelper.SendInvoiceEmail(orderId);
    }
}

private bool ConfirmOrder(Guid orderId)
{
    if (ConfirmPayment(orderId) == false)
    {
        return false;
    }

    try
    {             
        Order order = this.ordersManager.GetOrder(orderId);

        // Get existing customer record or create a new customer record
        Customer customer = ShoppingCartHelper.GetCustomerInfoOrCreateOneIfDoesntExsist(UserProfileManager, ordersManager, order);

        // Update the order with the customer and status
        order.Customer = customer;
        OrderStatus oldStatus = order.OrderStatus;
        order.OrderStatus = OrderStatus.Paid;
        this.ordersManager.SaveChanges();

        //raise here the status changed event handler directly
        ShoppingCartHelper.EcommerceEvents_OrderStatusChanged(order.Id, oldStatus, order.OrderStatus);

        PaymentResponse response = new PaymentResponse();
        response.IsSuccess = true;

        //OrderHelper.CapturePaymentResponse(order,         PayPalExpressHelper.PaymentProcessorName, response);

        ShoppingCartManager.CleanUp(orderId);

        OrderSuccess(orderId);
        return true;
    } // Try 
    catch (Exception ex)
    {
        Log.Write(ex, ConfigurationPolicy.ErrorLog);
        displayMessage(ex.Message);
        return false;
    }
}

private bool ConfirmPayment(Guid orderId)
{
    string payerId = "";
    string token = "";
    GetPayPalTokenAndPayerId(ref payerId, ref token);

    if (String.IsNullOrWhiteSpace(payerId) || String.IsNullOrWhiteSpace(token))
    {
        displayMessage("Unable to process order.");
        return false;
    }

    Order order = this.ordersManager.GetOrder(orderId);

    string defaultCurrency = Config.Get<ecommerceconfig>().DefaultCurrency;
    CurrencyCodeType currencyCodeType = PayPalExpressHelper.CurrencyCodeStringToType(defaultCurrency);

    DoExpressCheckoutPaymentResponseType response = PayPalExpressHelper.DoExpressCheckoutPayment(paypalExpressSettings, order.Total, currencyCodeType, token, payerId);

    if (response.Ack == AckCodeType.Success || response.Ack == AckCodeType.SuccessWithWarning)
    {
        return true;
    }
    else
    {
        displayMessage(PayPalExpressHelper.GetResponseErrors(response.Errors));
        return false;
    }
}

public static DoExpressCheckoutPaymentResponseType DoExpressCheckoutPayment(PayPalExpressSettings settings,
                                                                            decimal orderTotal, CurrencyCodeType currencyCode, string token, string payerId)
{
    DoExpressCheckoutPaymentRequestType request = new DoExpressCheckoutPaymentRequestType();
    request.DoExpressCheckoutPaymentRequestDetails = new DoExpressCheckoutPaymentRequestDetailsType();
    request.DoExpressCheckoutPaymentRequestDetails.Token = token;
    request.DoExpressCheckoutPaymentRequestDetails.PayerID = payerId;
    request.DoExpressCheckoutPaymentRequestDetails.PaymentAction = PaymentActionCodeType.Sale;
    request.DoExpressCheckoutPaymentRequestDetails.PaymentActionSpecified = true;

    request.DoExpressCheckoutPaymentRequestDetails.PaymentDetails = new PaymentDetailsType[1];
    request.DoExpressCheckoutPaymentRequestDetails.PaymentDetails[0] = new PaymentDetailsType();

    request.DoExpressCheckoutPaymentRequestDetails.PaymentDetails[0].OrderTotal =
    new BasicAmountType() { Value = orderTotal.ToString("0.00"), currencyID = currencyCode };

    CallerServices caller = new CallerServices();
    IAPIProfile profile = CreateApiProfile(settings);
    caller.APIProfile = profile;

    return (DoExpressCheckoutPaymentResponseType)caller.Call("DoExpressCheckoutPayment", request);
}

Ending

That's it. The code provided here is a little simplified, but should be able to get the basic idea. There are multiple ways to enhance and improve this scenarios and suit it to your needs. I'm not sure whether my explanations are sufficient enough, but do not hesitate to drop a comment and ask me, if you need further assistance.