Issues

Building a Webshop in Vendr

Ecommerce is a critical part of many websites, but it’s not something that is served by the Umbraco core, instead Umbraco HQ have focused developer resources on building the CMS product, and developing cloud and headless options.

So brands have been left to integrate Umbraco with an off-platform solution such as Shopify, or to use one of three third-party Umbraco packages, Tea Commerce, UCommerce and Merchello. There are substantial differences between each of them, but chiefly Tea Commerce and UCommerce were paid-for proprietary systems and Merchello open source.

With the launch of Umbraco 8 and it’s substantially rewritten API, a lot of work was required by most package vendors to enable their packages to keep up. UCommerce, with its strong commercial team, was quick to port their package to Umbraco 8. Indeed until the last couple of weeks, it was the only Ecommerce package available on Umbraco 8. That is, until the release of Vendr.

Vendr is the new kid on the block in terms of ecommerce on Umbraco - but it has a strong pedigree and hails from a strong stable. In 2019 Outfield Digital, the agency of MVPs and package guru’s Matt and Lucy Brailsford, bought the rights to Tea Commerce from Danish Gold Partner Tea Solutions.

When we discussed the project with Matt and Lucy, they said,

“We've worked in services for years, but have always enjoyed package development and had been looking to see if we could move into the commercial package space. When we heard Tea Solutions were looking for someone to take over the project, it felt like a great fit as we were already users of the product and it just felt like perfect timing.”

Matt has continued to support Tea Commerce on Umbraco 7, while at the same time redeveloping it to be Umbraco 8 by design. So significant is the rewrite, that a new product name was deserved, and Vendr was born.

Matt’s goal with Vendr is:

“To make ecommerce simpler. We've used quite a lot of eCommerce systems and they just always seem overly complex with options for every possible thing you would need, but as I've learnt in general package development, sometimes being simpler but extendable is the better approach, so that's what we want to bring to Vendr.”

According to Matt, the package is targeted at small to medium sized businesses, saying:

“...which I think make up the majority of Umbraco builds (for us at least). We don't have a restriction on the types of businesses Vendr is best for as it really works well in many different scenarios, be that retail products, digital goods, events, and others. If you can sell it online, Vendr will handle it.”

Features

Vendr is a comprehensive ecommerce platform for Umbraco 8, with product data stored as Umbraco content, so those familiar with Tea Commerce will feel comfortable transitioning.  This makes it possible for developers with familiarity with developing the front end of a site, to easily implement substantial parts of an ecommerce site without having to learn a new api.

Vendr provides complete control over the checkout process - so there’s no fear of the platform getting in the way of conversion optimisation.  It also supports product variants and related products, and manages product inventory as orders are made.

It includes a rules engine for managing special offers, and supports a number of different offer types out of the box.  

Having been developed so recently, and as you’d expect from a Matt Brailsford package, the editor experience is slick, with a well designed back end interface with order and discount management.

The system also provides a comprehensive settings panel with detailed security permissions.

Vendr multilingual and multi-currency.  

Shop administrators also have the ability to set up multiple stores, with different tax setups. Although we’re keen to investigate how this will work with complex tax regimes such as the USA. Vendr allows you to choose different shipping methods for different countries with the ability to add a friendly image, e.g. royal mail logo.

Vendr allows provides a templated email system, enabling administrators to specify different email templates for different statuses of the orders.

Vendr current ships with payment providers for DIBS, Paypal and Stripe as well as an Invoice / Offline Payment provider. We’ve used Stripe which was very easy to setup using webhooks to process orders back into our site, although our sandbox app uses the Invoice / offline payment provider.

There is also an API to enable devs to create their own payment providers for Vendr, full documentation is also provided.

Alternative Ecommerce Packages

If you want to integrate ecommerce into Umbraco 8, your choices really are limited to Vendr and UCommerce.

We’ve produced a feature by feature comparison of the two products on Google Sheets.  There are significant differences between the two products, not least that Vendr has a single version, and UCommerce offers a free, Pro and Enterprise edition.

From Outfield’s point of view, the key points of differentiation between Vendr and UCommerce:

“... is that we use Umbraco nodes for products. It does come with its limits, but I think for many people, the benefits will far outweigh the negatives. By using Umbraco nodes we integrate much deeper allowing you to use all of your favorite property editors and to structure your store exactly as you need it, meaning things just fit the end customer much better. Using Umbraco nodes also has the added benefit that we work well with other commercial packages OOTB including popular packages such as Translation Manager and uMarketingSuite.”

Vendr has a single pricing model, £1,500 for a perpetual license per site (excluding VAT) which includes 12 months of updates.  

UCommerce, which has a three level pricing model including a free tier, or either €2,999 or €4,999 for a single server, one year term license.

It’s difficult to compare prices across such different model, but broadly simple sites with no requirement for discounting could be free on UCommerce, but more complex sites that require multi-lingual or load-balancing or Umbraco Cloud support will be substantially cheaper on Vendr.

Setting up our Sandbox Site

We are in the process of designing and building an ecommerce site for a global consumer electronics manufacturer.  There is a requirement for ecommerce, and our goal is to use Umbraco 8, so in January we undertook a Proof of Concept of Vendr to ensure that it can meet our clients needs.

This Vendr Sandbox, which we’ve subsequently released under MIT license on Github, is intentionally simple but has the key functionality that is required to meet our requirements.  The site features products and product variants, a shopping cart with steps and payment by invoice rather than integrating with a payment gateway.

Much of the code used in our site uses demo code from Matt’s demo site, including Adding Products to Cart and applying discount codes.  However, we’ve substantially rewritten the demo in order to be able to get under the hood of Vendr and ensure we understand how it works, and also to meet our standard approach to building a site such as engineering out the use of Tailwind as we use Bootstrap as the basis of our sites.

In our Sandbox site we’ve created:

  • Site homepage DocType and Template;
  • Products which are normal Umbraco document types and content
    • We’ve used the Vendr price property editor
  • Product variants and inventories for product variants.
  • Full checkout process
    • Step by Step process
      • Ability to move back and forward in the checkout process
    • Payment providers
      • Pay by Invoice (In the Sandbox Repository); and
      • Stripe with webhooks for order updating (not included in our Sandbox)
    • Delivery options
      • Pickup
      • 1st class delivery (with additional costs)
  • Full basket functionality
    • With the ability to update quantity, remove items and checkout

Our Approach

We took the following approach to our building our Sandbox: 

  • Install Vendr;
  • Setup first store
    1. Emails
    2. Shipping
    3. Payment providers
  • Setup products
  • Setup Checkout using our own document types as opposed to the new Vendr Checkout nuget package
  • Setup our codebase

Code Review

We’ve made our code available as an MIT Licensed open source project, which can be cloned from Github.

SurfaceControllers

CartSurfaceController

This controller deals with the shopping basket of the website, and includes methods such as Cart Count, Add to Cart, Update Cart and Remove From Cart.

Example:

[HttpPost]
	[ValidateAntiForgeryToken]
	public ActionResult AddToCart(AddToCartDto postModel)
	{
		try
		{
			using (var uow = _uowProvider.Create())
			{
				var store = CurrentPage.GetStore();
				var order = _sessionManager.GetOrCreateCurrentOrder(store.Id)
					.AsWritable(uow)
					.AddProduct(postModel.ProductReference, 1);

				_orderService.SaveOrder(order);

				uow.Complete();
			}
		}
		catch (ValidationException)
		{
			ModelState.AddModelError("productReference", "Failed to add product to cart");

			return CurrentUmbracoPage();
		}

		TempData["addedProductReference"] = postModel.ProductReference;

		return RedirectToCurrentUmbracoPage();
	}

This was taken from the Demo site and we added ValidateAntiForgeryTokens.

CheckoutSurfaceController

This controller deals with the checkout and features applying and removing discounts, updating order information and updating shipping and payment methods.

These are customisable for your specific journey, and work alongside the UnitOfWork for Vendr updating via the Vendr Fluent API.

Example of updating order information, which allows customisation. We’ve added the extra property to populate the IP address.

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult UpdateOrderInformation(UpdateOrderInformationDto model)
{
	try
	{
		using (var uow = _uowProvider.Create())
		{
			var store = CurrentPage.GetStore();
			var order = _sessionManager.GetOrCreateCurrentOrder(store.Id)
				.AsWritable(uow)
				.SetProperties(new Dictionary<string, string>
				{
					{ Constants.Properties.Customer.EmailPropertyAlias, model.Email },
					{ "marketingOptIn", model.MarketingOptIn ? "1" : "0" },

					{ Constants.Properties.Customer.FirstNamePropertyAlias, model.BillingAddress.FirstName },
					{ Constants.Properties.Customer.LastNamePropertyAlias, model.BillingAddress.LastName },
					{ "billingAddressLine1", model.BillingAddress.Line1 },
					{ "billingAddressLine2", model.BillingAddress.Line2 },
					{ "billingCity", model.BillingAddress.City },
					{ "billingZipCode", model.BillingAddress.ZipCode },
					{ "billingTelephone", model.BillingAddress.Telephone },

					{ "shippingSameAsBilling", model.ShippingSameAsBilling ? "1" : "0" },
					{ "shippingFirstName", model.ShippingSameAsBilling ? model.BillingAddress.FirstName : model.ShippingAddress.FirstName },
					{ "shippingLastName", model.ShippingSameAsBilling ? model.BillingAddress.LastName : model.ShippingAddress.LastName },
					{ "shippingAddressLine1", model.ShippingSameAsBilling ? model.BillingAddress.Line1 : model.ShippingAddress.Line1 },
					{ "shippingAddressLine2", model.ShippingSameAsBilling ? model.BillingAddress.Line2 : model.ShippingAddress.Line2 },
					{ "shippingCity", model.ShippingSameAsBilling ? model.BillingAddress.City : model.ShippingAddress.City },
					{ "shippingZipCode", model.ShippingSameAsBilling ? model.BillingAddress.ZipCode : model.ShippingAddress.ZipCode },
					{ "shippingTelephone", model.ShippingSameAsBilling ? model.BillingAddress.Telephone : model.ShippingAddress.Telephone },

					{ "comments", model.Comments },
					{ "ipAddress", GetIPAddress() }

				})
				.SetPaymentCountryRegion(model.BillingAddress.Country, null)
				.SetShippingCountryRegion(model.ShippingSameAsBilling ? model.BillingAddress.Country : model.ShippingAddress.Country, null);

			_orderService.SaveOrder(order);

			uow.Complete();
		}
	}
	catch
	{
		ModelState.AddModelError("", "Failed to update information");
		return CurrentUmbracoPage();
	}

	if (model.NextStep.HasValue)
		return RedirectToUmbracoPage(model.NextStep.Value);

	return RedirectToCurrentUmbracoPage();
}

private string GetIPAddress()
	{
		var context = System.Web.HttpContext.Current;
		string ipAddress = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

		if (!string.IsNullOrEmpty(ipAddress))
		{
			string[] addresses = ipAddress.Split(',');
			if (addresses.Length != 0)
				return addresses[0];
		}
		return context.Request.ServerVariables["REMOTE_ADDR"];
	}

RenderMvcControllers

As standard, the following functionality comes coupled in Models on the Demo site, but due to the way we work, we’ve broken them into RenderMvcControllers and builders. This allows us to have more flexibility and control on how these are actioned.

This shows how flexible Vendr is and how much customisation you have, even with the project scaffolding.

Each of our controllers, uses a generic interface for a builder to populate and return a Model with the right information to render the page. This means the logic isn’t tightly coupled to the Model.

public interface IBuilder where T : IPublishedContent
    {
        T Build(T model);           
    }

This also keeps our Controllers nice and slim:

public class CartPageController : MasterController
    {
        private readonly IBuilder _builder;
        public CartPageController(IBuilder masterBuilder, IBuilder builder) 
            : base (masterBuilder)
        {
            _builder = builder ?? throw new ArgumentNullException(nameof(builder));
        }

        public override ActionResult Index(ContentModel model)
        {
            var viewModel = _builder.Build(model.Content as CartPage);
            return base.Index(new ContentModel(viewModel));
        }
    }

Each builder is then registered in our IoC:

composition.Register<IBuilder, CartPageBuilder>();
composition.Register<IBuilder, CheckoutPageBuilder>();
composition.Register<IBuilder, CheckoutInformationPageBuilder>();
composition.Register<IBuilder, CheckoutShippingPaymentMethodPageBuilder>();
composition.Register<IBuilder, CheckoutReviewPageBuilder>();
composition.Register<IBuilder, CheckoutConfirmationPageBuilder>();

Our builder replicates the original functionality in the models, but with more error checking.

CheckoutConfirmationController - CheckoutConfirmationBuilder

Populates:

  1. CheckoutPage
  2. FinalizedOrder
  3. Checkout Steps
public class CheckoutConfirmationPageBuilder : IBuilder
    {
        public CheckoutConfirmationPage Build(CheckoutConfirmationPage model)
        {
            if (model == null) throw new ArgumentNullException(nameof(model));

            model.CheckoutPage = model.GetCheckoutPage();
            var order = VendrApi.Instance.GetCurrentFinalizedOrder(model.GetStore().Id);            
            if (order != null)
            {
                model.Order = order;
                model.PaymentCountry = VendrApi.Instance.GetCountry(order.PaymentInfo.CountryId.Value);
                model.ShippingCountry = VendrApi.Instance.GetCountry(order.ShippingInfo.CountryId.Value);
            }

            var steps = model.GetCheckoutPage()?.GetSteps();
            if (steps != null)
                model.Steps = new StepsViewModel(steps, model.Id);

            return model;
        }
    }

The other controllers and builders do very similar work, with different properties being populated:

  • CartPageController - CartPageBuilder - Simply populates the CheckoutPage and Order information;
  • CheckoutInformationPageController - CheckoutInformationPageBuilder Populates: CheckoutPage, CartPage, CurrentOrder, Countries, Checkout Steps;
  • CheckoutPageController - CheckoutPageBuilder Populates: CurrentOrder
  • CheckoutReviewPageController - CheckoutReviewPageBuilder Populates: CheckoutPage, CurrentOrder, Checkouts Steps
  • CheckoutShippingPaymentMethodPageController - CheckoutShippingPaymentMethodPageBuilder Populates: CheckoutPage, CurrentOrder, Shipping Methods, Payment Methods, Checkouts Steps

All of the above builders use a set of PublishedContentExtensions to grab the right information from Umbraco or Vendr.

For example:

From Umbraco

public static Homepage GetHomepage(this IPublishedContent content)
		=> content.AncestorOrSelf();

	public static CheckoutPage GetCheckoutPage(this IPublishedContent content)
	=> content.AncestorOrSelf()?.FirstChild(x => x.IsPublished());

From Vendr:

public static OrderReadOnly GetCurrentOrder(this IPublishedContent content)
		=> VendrApi.Instance.GetCurrentOrder(content.GetStore().Id);

This keeps things tidy and helps us not to repeat our code.

As we’re hijacking the routes of these pages, we have our corresponding views, which we’ve taken from the demosite, slimmed down to use bootstrap with added AntiForgeryTokens.

Other References

Conclusion

We’re really pleased with Vendr, and have selected it as our ecommerce platform for our next project.  We found that it has the features that we need (critically product catalogue, multi-lingual, multi-currency, and support for load balancing).  The API is incredibly easy to work with, and we’ve found the documentation to be well written - which is a welcome change for Umbraco packages, especially so early in their life.

Matt, as always, has been responsive to bug reports, working with us to resolve bugs quickly.

We feel that the limited number of payment providers is the biggest impediment to adoption of Vendr at the moment.  However, from our point of view, getting Stripe and Paypal into v1 will meet the needs of many sites.

The development of Vendr has been a big effort by Matt and Lucy, and they’ve invested significant time and money in the platform.  But as any of us know in the Umbraco community, releasing a package doesn’t mean the project is done.  Outfield are committed to the ongoing support of the package, and have a product road map which sees significant new feature development over the next 12 months.

Matt said:

“It's a bit of a tricky time in the world at the moment so our roadmap is a little flexible, but in the short term we are listening to the devs who are building with Vendr already and fixing any issues that might pop up and are looking at what features would help improve their experience.

And in the longer term there are a number of features still to port over from Tea Commerce, such as multi variants (though we are waiting a little bit here to see what the Umbraco block editor brings) as well as more Payment Providers to create and docs to write. In addition to this there are also a number of add-on packages we are creating too such as Vendr Checkout and Vendr Cart to really help simplify eCommerce development. So yea, a lot going on, but we are excited to see what the future brings.”

Having both worked with Matt, and used Tea Commerce for years, we’re convinced they’re a match made in heaven, and that as a result Vendr will be a strong foundation on which to build ecommerce sites on Umbraco.

Gareth Wright

Gareth is an Umbraco Certified Expert and has over 20 years experience in development working in digital agencies on best of breed websites. Gareth is Development Manager at Carbon Six Digital.

Paul Marden

Paul is the Director of Carbon Six Digital and a long time fan of Umbraco. He's been building Umbraco powered sites since 2008. Carbon Six Digital are Umbraco Gold Partners and focus on building Editor optimised Umbraco websites so marketers can focus on generating leads not wrangling with a rubbish CMS.

comments powered by Disqus