Tuesday, October 30, 2012

Adding microdata format to your blog posts with Sitefinity

Our team @Telerik is currently busy with the migration of blogs.telerik.com to the latest version of Sitefinity 5.x. This task comes with some challenges, one of which is the improvement of the markup rendered by Sitefinity - we would like to make it semantic using schema.org. I already showed how to decorate your breadcrumb control with microdata format. To achieve the same for blog posts is just a little more work.

Here is an example of how a blog post should look like according to schemata.org:
<body itemscope itemtype="http://schema.org/Blog">
<!-- Blog post -->
    <article itemprop="blogPost">
        <header>
     <h1 itemprop="headline">Headline</h1>  
         <time itemprop="datePublished" datetime="2012-08-08">Wednesday, August 08, 2012</time>
  by 
  <a href="#" itemprop="author">Just* Team</a> | <a href="">Comments 0</a>
 </header>

<p>Blog post body paragraph 1</p>

<p>Blog post body paragraph 2</p>

    <section>
 <h2>2 Comments</h2>
 <article class="comment" itemscope itemtype="http://schema.org/UserComments">
           <div>
  <span itemprop="creator">Dejan</span>
  <time itemprop="commentTime" datetime="2012-08-09">09 Aug 2012</time>
     </div>

     <p itemprop="commentText">Comment text 1</p>
 </article>

 <article class="comment" itemscope itemtype="http://schema.org/UserComments">
     <div>
  <span itemprop="creator">Dejan</span>
  <time itemprop="commentTime" datetime="2012-08-09">09 Aug 2012</time>
     </div>

 <p itemprop="commentText">Comment text 2</p>
   </article>

  </section>
 </article>
<!-- Blog post -->
</body>
Notice that we have to add some attributes to the body tag. One way to achieve this is to use a master page. This will eventually lead to having a lot of master pages with just a different body tag for every content type that you have in your website. Another way is to create a custom control that renders these attributes for you - again quite inconvenient as you have to remember to drag this control on every page you need semantic markup ( and nothing will alert you that you have forgotten somewhere). The approach we choose to take is to derive from the Details view class for blog posts and override the PreRender event handler:
public class DetailPostViewSemanticMarkup : DetailPostView
    {
        protected override void OnPreRender(System.EventArgs e)
        {
         base.OnPreRender(e);

            if (this.Page != null)
            {
                HtmlGenericControl ctrlBody = this.Page.FindControlRecursively("grid") as HtmlGenericControl;
                if (ctrlBody != null)
                {
                    ctrlBody.Attributes.Add("itemtype", @"http://schema.org/Blog");
                    ctrlBody.Attributes.Add("itemscope", "itemscope");
                }
                else
                {
                    throw new ConfigurationException("The configured master page doesn't have <body runat=\"server\", which is required by the selected detail template for blogs");
                }
            }
        }
    }
After that you need to make Sitefinity to use this template for your details pages:
Go to the Administration and navigate to Settings > Advanced > Blogs > Controls > BlogPostsFrontend > DetailBlogPostsFrontend and change the property called "ViewType" to the fully qualified type name of the new class you created (in my case "BlogsWebApp.Views.Blogs.DetailPostViewSemanticMarkup"). After saving this setting you should be able to see the desired attributes in the body tag of the details pages in your blogs.

Next, navigate Design > Widget Templates and find the templates that is used in your blogs detail pages, by default it is called: "Full blog post item" and it applies to Blog posts - single. Go to edit it and change it in the way that fits the above specification. Our template looks similar to:
<%@ Control Language="C#" %>
<%@ Register TagPrefix="telerik" Namespace="Telerik.Web.UI" Assembly="Telerik.Web.UI" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI" Assembly="Telerik.Sitefinity" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI.ContentUI" Assembly="Telerik.Sitefinity" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI.PublicControls.BrowseAndEdit"
    Assembly="Telerik.Sitefinity" %>
<%@ Import Namespace="Telerik.Sitefinity" %>
<telerik:RadListView ID="SingleItemContainer" ItemPlaceholderID="ItemContainer" AllowPaging="False"
    runat="server" EnableEmbeddedSkins="false" EnableEmbeddedBaseStylesheet="false">
    <layouttemplate>
        <article itemprop="blogPost">
            <asp:PlaceHolder ID="ItemContainer" runat="server" />
        </article>
    </layouttemplate>
    <itemtemplate>
        <header>
          <h1 itemprop="headline" class="mb20">
            <%# Eval("Title")%>
          </h1>
        </header>
        <div class="sfpostAuthorAndDate cuArticleInfo mb20">
            <time itemprop="datePublished" datetime='<%#((DateTime)Eval("PublicationDate")).ToString("yyyy-MM-dd")%>'>
               <%# ((DateTime)Eval("PublicationDate")).ToString("dddd, MMMM dd, yyyy")%>
            </time>

            <asp:Literal ID="Literal2" Text="<%$ Resources:Labels, By %>" runat="server" /> 
            <span itemprop="author">  
              <sf:PersonProfileView runat="server" /> 
            </span>
          |  <a href="#comments" class="comments">Comments <span class="bubble"><span class="point"></span><span class="bubble-icon rounded3">000</span></span></a>
        </div>
        <sf:ContentBrowseAndEditToolbar ID="BrowseAndEditToolbar" runat="server" Mode="Edit,Delete,Unpublish"></sf:ContentBrowseAndEditToolbar>
        <sf:FieldListView ID="PostContent" runat="server" 
            Text="{0}" Properties="Content" 
            WrapperTagName="div" WrapperTagCssClass="sfpostContent tPostContent mb40"
        />

        <asp:PlaceHolder ID="socialOptionsContainer" runat="server">
  
    </asp:PlaceHolder>
        <sf:ContentView 
             id="commentsListView" 
             ControlDefinitionName="BlogsCommentsFrontend"
             DetailViewName="CommentsMasterView" 
             ContentViewDisplayMode="Master"
             LayoutTemplatePath="~/ExternalTmpl/Comments/CommentsMasterView.ascx"
             runat="server" />
        <sf:ContentView 
             id="commentsDetailsView" 
             ControlDefinitionName="BlogsCommentsFrontend" 
             DetailViewName="CommentsDetailsView"
             ContentViewDisplayMode="Detail"
            LayoutTemplatePath="~/ExternalTmpl/Comments/CommentsDetailsView.ascx"
             runat="server" />
    </itemtemplate>
</telerik:RadListView>

Finally, we have to take care for the comments. Mind that we use external template for them in order to suit it for our needs:
<%@ Control Language="C#" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI.ContentUI" Assembly="Telerik.Sitefinity" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI" Assembly="Telerik.Sitefinity" %>
<%@ Register TagPrefix="telerik" Namespace="Telerik.Web.UI" Assembly="Telerik.Web.UI" %>

<telerik:RadListView ID="commentsList" ItemPlaceholderID="ItemsContainer" runat="server" EnableEmbeddedSkins="false" EnableEmbeddedBaseStylesheet="false">
    <LayoutTemplate>
        <section>
            <h2 id="comments" class="sfcommentsTitle">
                <asp:Literal ID="comments" runat="server" />
            </h2>
            <div class="tCommentListing">
                <asp:PlaceHolder ID="ItemsContainer" runat="server" />
            </div>
        </section>
    </LayoutTemplate>
    <ItemTemplate>
        <article itemscope itemtype="http://schema.org/UserComments" class="sfcommentDetails">
            <div class="tCommentAuthor">
                <sf:SitefinityHyperLink ID="website" Text='<%# Eval("AuthorName") %>' NavigateUrl='<%# Eval("Website") %>' runat="server" Target="_blank" />
                <span itemprop="creator" id="authorName" runat="server">
                    <%# Eval("AuthorName") %>
                </span>
                <time class="tCommentDate" itemprop="commentTime" datetime="<%#((DateTime)Eval("DateCreated")).ToLocalTime().ToString("dd MMM") %>">
                    <sf:FieldListView ID="dateCreated" runat="server" Format="{DateCreated.ToLocal():dd MMM}" />
                </time>
            </div>
            <div class="tCommentText" itemprop="commentText">
                <asp:Literal ID="content" Text='<%# Eval("Content") %>' runat="server" />
            </div>
        </article>
    </ItemTemplate>
</telerik:RadListView>

That's all you need. One tiny remark: I couldn't find a way to add itemscope attribute only to the body tag without adding value for it. From what I found in the internet, it's perfectly ok to replace itemscope with itemscope="itemscope" and that what I did. I hope you find this helpful.

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.

Wednesday, October 17, 2012

Adding regions to your xml files in Visual Studio

Most of the website we maintain and develop at Telerik, needs the ability to redirects urls. Because of this, there are lots of legacy urls and the files that describe those redirects quickly get enormous (thousand of lines). Unfortunately, Visual Studio doesn't gives you the option to organize xml documents into regions as with the c# files.

Luckily, I stumbled upon a very useful extension for Visual Studio that does exactly this. It is presented as tool for organizing XAML files, but actually it deals with every xml document. I already tried it and it works great with both VS 2010 and VS 2012.

The syntax is pretty much the same as we know it:
<!-- #Region Some Useful Label here -->
//your xml document
<!-- #EndRegion -->

Check it out: http://inchoatethoughts.com/xamlregions or grab it from Visual Studio gallery. Big Thanks to the author Jacob Johnston!

Monday, October 15, 2012

The making of DevReach.com - Part 3, the eCommerce module

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, we created to new modules "Speakers" and "Lectures" with a relation between them. After that, we customized the front-end of these modules to fit our designs. Now, it's time to setup the eCommerce module and to build the registration process.

The registration process

For this edition of DevReach, together with our information architect and our event manager, we built entirely new registration process. We had main four steps, which were as follows:

1. Choosing how many passes of each type you wish and entering coupon code (if you have one).
2. Assigning the selected passes to people. This step was optional and this could be done after the registration is over as well.
3. Entering billing details - names, company information, invoice information, etc.
4. Choosing payment method.

Unfortunately, this workflow didn't fit very well in what Sitefinity eCommerce module offers out of the boxes. For, example step 2 is something that very little to do with the eCommerce module. Also, we wanted the coupon code to be entered before everything else in the shopping cart is done. The product list on our wire-frames was grouped by product type and sorted by a new field added by as - DisplayIndex. And many other tiny difference we had. Fortunately, the eCommerce module offers an API that basically gives you to do almost everything you wish using custom widgets and this is exactly what we did.

Step 1 - Choosing the products and entering the coupon code

This year we had two main products, which were basically the passes we offer - standard and vip. And we had some additional products to them, which were the various workshop that DevReach offered this for the first time. Every main product can be combined with every additional product (with some additional constraints). So, we decided to create two product types - conference pass and conference addon.
public IList<Product> GetProductsFromGivenType(ProductTypeEnum productTypeEnum)
{
      string productTypeTitle = productTypeEnum.ToProductTypeTitle();

      ProductType productType = this.catalogManager.GetProductTypes()
          .FirstOrDefault(pt => pt.Title == productTypeTitle);

      return catalogManager
           .GetProducts(productType.ClrType)
           .Where(p => p.IsActive)
           .OrderBy(p => p.FieldValue<decimal>("DisplayIndex"))
           .ToList();
}
This function takes an Enum as a parameter and retrieves that title of the product type and corresponds to this enum value. Before showing these products to the user, we have to get all active site-wide discounts and show the old and the new price. This is easy:
    public static IQueryable<Discount> GetDiscounts()
    {
        OrdersManager ordersmanager = OrdersManager.GetManager();
        return ordersmanager.GetDiscounts();
    }
And on item data bound for every product:
decimal newPrice = product.DisplayPrice;
foreach (Discount discount in SitewideDiscounts)
{
    Literal lRibbons = liHolder.FindControl("lRibbons") as Literal;
    lRibbons.Text += String.Format("<span class='ribbon'>{0}</span>", discount.Title);

    if (discount.DiscountAmountType == DiscountAmountType.Percent)
    {
        newPrice = newPrice - (newPrice * discount.DiscountAmount / 100);
    }
    else
    {
        newPrice = newPrice - discount.DiscountAmount;
    }
}

Label lnewPrice = e.Item.FindControl("lNewPrice") as Label;
lnewPrice.Text = String.Format("{0:0.##} EUR <span class='vat db '>including VAT*</span>", newPrice);

if (product.DisplayPrice > newPrice)
{
    Label loldPrice = e.Item.FindControl("loldPrice") as Label;
    loldPrice.Text = String.Format("{0:0.##} EUR", product.DisplayPrice);        }

On submit of this page, we have to gather all provided information and save it in the current order (or create new order if non exists).
public static CartOrder GetShoppingCartForUser(OrdersManager ordersManager)
{
    Guid shoppingCartId = GetShoppingCartId();
    CartOrder shoppingCart = ordersManager.TryGetCartOrder(shoppingCartId);
    if (shoppingCart == null)
    {
       shoppingCartId = Guid.NewGuid();
       RemoveShoppingCartCookie();
       SetShoppingCartId(shoppingCartId);
       shoppingCart = ordersManager.CreateCartOrder(shoppingCartId, null);
       ordersManager.SaveChanges();
    }

    return shoppingCart;
}

public static void RemoveShoppingCartCookie()
{
     HttpCookie httpCookie = new HttpCookie(EcommerceConstants.OrdersConstants.ShoppingCartIdCookieName, "");
     DateTime now = DateTime.Now;
     httpCookie.Expires = now.AddDays(-1);
     HttpContext.Current.Response.Cookies.Add(httpCookie);
}

Adding orderDetails for each product or updating the quantity if it already exists:
foreach (Product product in passes)
{
    int quantity = productQuantities[product.Id];
    CartDetail cartDetail = cartOrder.Details.SingleOrDefault(d => d.ProductId == product.Id);
    //if such product exists in the order
    if (cartDetail != null)
    {
        orderManager.SetQuantity(cartDetail, quantity);
    }
    else
    {
       OptionsDetails optionsDetails = new OptionsDetails();

       cartOrder.Currency = Config.Get<EcommerceConfig>().DefaultCurrency;
       orderManager.AddToCart(cartOrder, product, optionsDetails, quantity);
    }
}

And if a discount code is entered, we have to validate it:
var couponCodeValidator = new CouponCodeValidator(orderManager);
 if (couponCodeValidator.IsCouponCodeValid(couponName, cartOrder.SubTotalDisplay, out negativeMessage))
    {
          cartOrder.CouponCodes.Add(new CouponCode { Code = couponName });
    }
    else
    {
          if (oldCouponName != null)
          {
               cartOrder.CouponCodes.Add(new CouponCode { Code = oldCouponName });
          }
     }

Step 2 - Adding attendees to the order

For this step, we created a new module (using the module builder) called Attendees, which we use as a journal for attendees records. All attendees (names, email, pass type, etc.) for an order are kept here. Once the order is marked paid, from this data we create public users in the CMS and attach the users to the attendees. This way we have relationship between user and an order. We have created an hierarchy of objects for easier work with dynamic modules, but I will write about this some other time.

Step 3 - Billing details page

This page is probably the one that is most close to what the cms does. Nothing interesting to explain here:
void rbProceedPayment_Click(object sender, EventArgs e)
{
    if (rbInvoice.Checked)
    {
        if (!cbSameAsBilling.Checked)
        {
            Page.Validate("shippingAddress");
        }
        Page.Validate("billingAddress");

        if (rbCompanyInfo.Checked)
        {
            Page.Validate("companyInfo");
        }
        else
        {
            Page.Validate("personalInfo");
        }
    }

    if (Page.IsValid)
    {
        CartOrder cartOrder = ShoppingCartManager.GetShoppingCartForUser();
        cartOrder.Addresses.Clear();
        OrdersManager orderManager = new OrdersManager();
                    
        CartAddress billingAddress = orderManager.CreateCartAddress();
        billingAddress.FirstName = txtFirstName.Text;
        billingAddress.LastName = txtLastName.Text;
        billingAddress.Phone = txtPhone.Text;
        billingAddress.Email = txtEmail.Text;
        billingAddress.AddressType = AddressType.Billing;
        billingAddress.Address = "";
        billingAddress.City = "";
        billingAddress.Country = "";
        billingAddress.PostalCode = "";
        billingAddress.Company = "";

        if (rbInvoice.Checked)
        {
            billingAddress.Address = txtBillingStreet.Text;
            billingAddress.City = txtBillingCity.Text;
            billingAddress.Country = txtBillingCountry.Text;
            billingAddress.PostalCode = txtBillingPostalCode.Text;
            billingAddress.Company = txtCompanyName.Text;

            CartAddress shippingAddress = orderManager.CreateCartAddress();
            shippingAddress.FirstName = txtFirstName.Text;
            shippingAddress.LastName = txtLastName.Text;
            shippingAddress.Phone = txtPhone.Text;
            shippingAddress.Email = txtEmail.Text;
            shippingAddress.Company = txtCompanyName.Text;
            shippingAddress.AddressType = AddressType.Shipping;

            if (cbSameAsBilling.Checked)
            {
                shippingAddress.Address = txtBillingStreet.Text;
                shippingAddress.City = txtBillingCity.Text;
                shippingAddress.PostalCode = txtBillingPostalCode.Text;
                shippingAddress.Country = txtBillingCountry.Text;
            }
            else
            {
                shippingAddress.Address = txtShippingStreet.Text;
                shippingAddress.City = txtShippingCity.Text;
                shippingAddress.PostalCode = txtShippingPostalCode.Text;
                shippingAddress.Country = txtShippingCountry.Text;
            }

            cartOrder.Addresses.Add(shippingAddress);
        }

        cartOrder.Addresses.Add(billingAddress);
        orderManager.SaveChanges();

        Response.Redirect("~/register/checkout");
    }
}

Step 4 - Choosing the payment method

On this step we had to retrieve from the API all active payment methods and show them to the user:

                IQueryable<PaymentMethod> paymentMethods = ordersManager.GetPaymentMethods().Where(method => method.IsActive);

                rlvPaymentMethods.DataSource = paymentMethods;
                rlvPaymentMethods.DataBind();

The interesting part is when the Place Order button is clicked:
void btnPlaceOrder_Click(object sender, EventArgs e)
{
    Guid selectedPaymentMethodGuid = new Guid(Page.Request["paymentMethods"]);
    PaymentMethod paymentMethod = ordersManager.GetPaymentMethod(selectedPaymentMethodGuid);

    if (paymentMethod == null)
    {
        throw new ConfigurationErrorsException("the selected payment method do not exists");
    }

    if (!paymentMethod.IsActive)
    {
        throw new ConfigurationErrorsException("the selected payment method is not active");
    }

    if (paymentMethod.PaymentMethodType == PaymentMethodType.Offline)
    {
        PlaceOfflineOrder(paymentMethod);
        EmailHelper.SendInvoiceEmail(ShoppingCartManager.GetShoppingCartId());
    }
    else
    {
        switch (paymentMethod.Title)
        {
            case "PayPal Express Checkout":
                PlacePayPalExpressOrder(paymentMethod);
                break;
            default:
                throw new InvalidOperationException("Not implemented online payment method: " + paymentMethod.Title);
        }
    }

    this.RemoveShoppingCartCookie();
}

private void PlaceOfflineOrder(PaymentMethod paymentMethod)
{
    CartOrder cartOrder = ShoppingCartManager.GetShoppingCartForUser();
            
    ShoppingCartManager.CleanNotPurchasedProducts(cartOrder);
    CheckoutState checkoutState = cartOrder.GetCheckoutState();
    checkoutState.PaymentMethodId = paymentMethod.Id;
    checkoutState.PaymentMethodType = paymentMethod.PaymentMethodType;

    CartPayment payment = ShoppingCartHelper.GetCartPaymentFromCheckoutState(ordersManager, checkoutState);
    cartOrder.Payments.Add(payment);
    ordersManager.SaveChanges();

    ShoppingCartManager.PlaceOfflineOrder(checkoutState);

    Page.Response.Redirect("~/register/thank-you-offline", false);
}

public static bool PlaceOfflineOrder(CheckoutState checkoutState, Guid cartOrderId)
{
    UserProfileManager userProfileManager = UserProfileManager.GetManager();
    OrdersManager ordersManager = OrdersManager.GetManager();
    try
    {
        User customerUser = null;
        Customer customer = ShoppingCartHelper.GetCustomerInfoOrCreateOneIfDoesntExsist(userProfileManager, ordersManager, checkoutState, out customerUser);
                  
        OrderValidator orderValidator = new OrderValidator { IsOrderValid = true, StatusMessage = string.Empty };
  
        if (orderValidator.IsOrderValid)
        {
            IPaymentResponse paymentResponse = ordersManager.Checkout(cartOrderId, checkoutState, customer);

            // record the "success" state of the checkout
            checkoutState.IsPaymentSuccessful = paymentResponse.IsSuccess;

            Order order = ordersManager.GetOrder(cartOrderId);
            IncrementOrderNumber(order, ordersManager);

            // add the order to customer
            customer.Orders.Add(order);

            // Update the order
            order.Customer = customer;
            order.OrderStatus = checkoutState.PaymentMethodType == PaymentMethodType.Offline ? OrderStatus.Pending : OrderStatus.Paid;
            ordersManager.SaveChanges();

            // Send an email to the customer.
            try
            {
                this.SendOrderPlacedEmailToClientAndMerchant(order);
            }
            catch (Exception emailEx)
            {
                // Do nothing. If sending email fails, we still want CleanUp to occur and the user to be directed to the next page.
                Log.Write(emailEx, ConfigurationPolicy.ErrorLog);
            }

            CleanUp(cartOrderId);
        }
        else
        {
            //this.PaymentProblemPanel.Visible = true;
            //this.MessageControl.ShowNegativeMessage(orderValidator.StatusMessage);
        }

        return orderValidator.IsOrderValid;
    }
    catch (Exception ex)
    {
        Log.Write(ex, ConfigurationPolicy.ErrorLog);
        //this.PaymentProblemPanel.Visible = true;
        //this.MessageControl.ShowNegativeMessage(ex.Message);
        return false;
    }
}
  
This is all that is needed to place an offline order. I will write a separate post for the paypal integration we did together with the eCommerce team.

The last thing that worth mentioning is that in order to create the users, once the order is marked as paid - we needed a hook to subscribe for this event. Fortunately for us, the product team introduced a new event for changing the status of an order which was exactly what we needed, no matter if the order is online or offline:

protected void Application_Start(object sender, EventArgs e)
{
  Bootstrapper.Initialized += new     EventHandler<Telerik.Sitefinity.Data.ExecutedEventArgs>(Bootstrapper_Initialized);
}

private void Bootstrapper_Initialized(object sender, Telerik.Sitefinity.Data.ExecutedEventArgs e)
{
  if (e.CommandName == "Bootstrapped")
  {
    EventHub.Subscribe<IEcommerceOrderStatusChangedEvent>(OnOrderStatusChanged);
  }
}

private void OnOrderStatusChanged(IEcommerceOrderStatusChangedEvent evt)
{
  var orderId = evt.OrderId;
  var oldOrderStatus = evt.OldOrderStatus;
  var newOrderStatus = evt.NewOrderStatus;
  if (newOrderStatus == OrderStatus.Paid)
  {
       if (!OrderHelper.IsOrderPaid(orderId))
       {
           AttendeeManager attendeeManager = new AttendeeManager();
           IList<Attendee> attendees = attendeeManager.GetAllPublishedByOrderId(orderId);
           UsersHelper.CreateCustomerUserAndSendActivationEmail(orderId, attendees);

           foreach (Attendee attendee in attendees)
           {
                UsersHelper.CreateUserAndSendActivationEmail(attendee, attendeeManager);
           }
       }
   }

   if (newOrderStatus == OrderStatus.Deleted)
   {
        OrderHelper.DeleteOrderInformation(orderId);
   }            
}

What's next?

Basically, this was our registration process. I skipped some of the code fore brevity and simplicity, but all the interaction with the eCommerce API is here. Next time I will show you the integration with paypal using the PayPal Express Checkout scenario. And stay tuned for the coming release of Sitefinity 5.2 - lots of new features and improvements will be released.