Building a Custom Shipping Method Service
One of my favorite features of Ucommerce is the extensible framework lurking underneath the surface. Using the framework you have the opportunity to add, modify, or even completely replace the out of the box functionality of Ucommerce. Shipping methods are no exception to this rule. Specifically you can build custom shipping method services to enable custom calculation of shipping costs during the checkout flow as discussed in Shipping Methods Explained.
This article covers adding a new shipping method service to Ucommerce and having it show up the admin UI like so.
The Framework
Before you get into the nitty-gritty of building your first custom shipping method service you’ll need a little background about the surrounding framework to understand what’s going on.
Your shipping method service first needs to be configured in Ucommerce via web.config (more on that later), once written the Ucommerce pipelines will make sure and trigger your service when appropriate. By default shipping methods services are triggered in the Basket Pipeline by the task called CalculateShippingCostForShipments. If you’re interested in more information about pipeline please see the Ucommerce Pipelines Explained article.
Notice that the tasks ends on Shipments plural. Ucommerce supports multiple shipments per order and will call the proper shipping method service for each shipment based on the shipping method selected for it.
Pretty simple stuff. Now for the fun part: Writing your own shipping method service.
The IShippingMethodService Interface
This interface is what Ucommerce uses to be able to calculate shipment costs. As you can see from the code it’s pretty simple. Basically you’ll receive the shipment as created using either the XSLT or .NET API and you’ll have the opportunity to loop through the relevant order lines, which are linked from the shipment. Let's see what’s involved to create a simple volume based IShippingMethodService.
Volume Based Shipping Method Service
Volume and weight-based shipping pricing are probably the most common shipping scenarios for an online store so let's try and build one of them as an example. First you’ll need to create a new Visual Studio project and add a reference to the UCommerce.dll assembly. You will also need a reference to the Subsonic.Core.dll assembly. With that in place you’re ready to start implementing the service.
The Methods and Properties
/// <summary> /// Interface for calculating shipping fees /// </summary> public interface IShippingMethodService { /// <summary> /// Calculates the shipping fee for a particular <see cref="Shipment"/> /// </summary> /// <param name="shipment"></param> /// <returns></returns> Money CalculateShippingPrice(Shipment shipment); /// <summary> /// Validate an exising <see cref="Shipment"/> for shipping /// </summary> /// <param name="shipment"></param> /// <returns></returns> bool ValidateForShipping(Shipment shipment); }
As you can see you just need to implement two methods and a single property to get it going. The names of the methods should make it fairly obvious what they’re for. ValidateForShipping might require some explanation.
As discussed in Shipping Method Explained each shipping method can be configured to available only to certain stores and for shipment only to certain countries. The ValidateForShipment enforces these rules. If you just need an unrestricted shipment you can go ahead and always return true, but be aware that users might be confused if they set up a shipping method in one way but it doesn’t actually check for the conditions thus behaving differently from what they expect.
CalculateShippingPrice Method
First up let's calculate the shipping price for the shipment:
public Money CalculateShippingPrice(Shipment shipment) { // First sum up the total weight for the shipment. // We're assumning that a custom order line property // was set on the order line prior when the product was added to the order line. decimal totalWeight = 0; foreach (OrderLine orderline in shipment.OrderLines) totalWeight += orderline.Quantity * Convert.ToDecimal(orderline["Weight"]); decimal shippingPrice = 0; if (totalWeight > 10) shippingPrice = 100; else if (totalWeight > 20) shippingPrice = 200; else shippingPrice = 300; // To instantiate a new Money object we need the currency, // which is set on the purchase order. To get the currency // we move through Shipment -> OrderLines -> PurchasrOrder -> Currency return new Money(shippingPrice, shipment.PurchaseOrder.BillingCurrency); }
Of course the business rule in this particular case is very simplistic, but you get the idea.
ValidateForShipping Method
Next up is the ValidateForShippingMethod, which is actually called prior to CalculateShippingPrice to validate that the shipping method is validate for the current purchase order. The SinglePriceShippingMethod which comes out of the box has rules to ensure that the shipping method has a shipping address set and that the shipment is set to be delivered to one of the allowed countries for the shipping method.
public bool ValidateForShipping(Shipment shipment) { if (shipment.ShipmentAddress.AddressId <= 0) throw new InvalidOperationException("Cannot validate shipment for country. Remember to set the shipping address for shipment."); var shippingMethod = shipment.ShippingMethod; if (shippingMethod == null) throw new InvalidOperationException( "Cannot validate destination country for shipment. It does not contain a shipping method. Remember to add a shipping method to your shipment before validating."); return ValidateShippingDestination(shipment.ShipmentAddress, shippingMethod); } /// <summary> /// Validates the order lines according to their desired destination and configured contries for shipping method /// </summary> /// <returns></returns> protected virtual bool ValidateShippingDestination(OrderAddress shippingAddress, ShippingMethod shippingMethod) { var eligibleCountries = shippingMethod.EligibleCountries; // No eligible countries exist - so the shipment isn't valid if (eligibleCountries == null) return false; return eligibleCountries.SingleOrDefault(x => x == shippingAddress.Country) != null; }
VolumeShippingMethodService
public class VolumeShippingMethodService : IShippingMethodService { public Money CalculateShippingPrice(Shipment shipment) { // First sum up the total weight for the shipment. // We're assumning that a custom order line property // was set on the order line prior when the product was added to the order line. decimal totalWeight = 0; foreach (OrderLine orderline in shipment.OrderLines) totalWeight += orderline.Quantity * Convert.ToDecimal(orderline["Weight"]); decimal shippingPrice = 0; if (totalWeight > 10) shippingPrice = 100; else if (totalWeight > 20) shippingPrice = 200; else shippingPrice = 300; // To instantiate a new Money object we need the currency, // which is set on the purchase order. To get the currency // we move through Shipment -> OrderLines -> PurchasrOrder -> Currency return new Money(shippingPrice, shipment.PurchaseOrder.BillingCurrency); } public bool ValidateForShipping(Shipment shipment) { if (shipment.ShipmentAddress != null) throw new InvalidOperationException("Cannot validate shipment for country. Remember to set the shipping address for shipment."); var shippingMethod = shipment.ShippingMethod; if (shippingMethod == null) throw new InvalidOperationException( "Cannot validate destination country for shipment. It does not contain a shipping method. Remember to add a shipping method to your shipment before validating."); return ValidateShippingDestination(shipment.ShipmentAddress, shippingMethod); } /// <summary> /// Validates the order lines according to their desired destination and configured contries for shipping method /// </summary> /// <returns></returns> protected virtual bool ValidateShippingDestination(OrderAddress shippingAddress, ShippingMethod shippingMethod) { var eligibleCountries = shippingMethod.EligibleCountries; // No eligible countries exist - so the shipment isn't valid if (eligibleCountries == null) return false; return eligibleCountries.SingleOrDefault(x => x == shippingAddress.Country) != null; } }
Registering Your ShippingMethodService with Ucommerce
You must also register the service by adding a new file to Ucommerce/Apps/MyApp
called Shipping.config with the following component:
<configuration> <components> <component id="VolumeShippingService" service="UCommerce.Transactions.Shipping.IShippingMethodService, UCommerce" type="MyUCommerceApp.VolumeShippingMethod, MyUCommerceApp"/> </components> </configuration>
With the app updated you will now be able to select your new shipping method service in the UI:
Summing It All Up
Creating your own shipping method service to handle custom calculations takes as little effort as implementing two methods and a property. Once it’s registered with Ucommerce users take over and set up the shipping method to their liking. Because your shipping method service is rolled into a separate assembly it is very straightforward to share between projects.
If you would like more information about Shipping Method please take a look at the article Shipping Methods Explained.