Integrating A Shipping Carrier

From AbleCommerce Wiki
Revision as of 13:29, 27 April 2009 by Mazhar (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

IShippingProvider

To integrate a shipping carrier with Ablecommerce 7 you need to implement CommerceBuilder.Shipping.Providers.IShippingProvider interface. This interface defines how shipping providers interact with Ablecommerce.

This IShippingProvider interface is defined as follows

    /// <summary>
    /// Interface to be implemented by Shipping Providers
    /// </summary>
    public interface IShippingProvider
    {
        /// <summary>
        /// Id of the shipping gateway in database. Id is passed at the time of initialization. 
        /// The implementation should return the same id.
        /// </summary>
        int ShipGatewayId { get; }

        /// <summary>
        /// Name of the shipping provider implementation
        /// </summary>
        string Name { get; }

        /// <summary>
        /// Version of the shipping provider implementation
        /// </summary>
        string Version { get; }

        /// <summary>
        /// Description of the shipping provider implementation
        /// </summary>
        string Description { get; }

        /// <summary>
        /// Gets a Url for the logo of the shipping provider implementation
        /// </summary>
        /// <param name="cs">ClientScriptManager</param>
        /// <returns>A Url for the logo of the shipping provider implementation</returns>
        string GetLogoUrl(System.Web.UI.ClientScriptManager cs);

        /// <summary>
        /// Gets a Url for the configuration of the shipping provider implementation
        /// </summary>
        /// <param name="cs">ClientScriptManager</param>
        /// <returns>A Url for the configuration of the shipping provider implementation</returns>
        string GetConfigUrl(System.Web.UI.ClientScriptManager cs);

        /// <summary>
        /// Indicates whether to use debugging mode or not?
        /// </summary>
        bool UseDebugMode { get; set; }

        /// <summary>
        /// Initializes the shipping provider implementation.
        /// This method is called by Ablecommerce at the time of initialization.
        /// </summary>
        /// <param name="shipGatewayId">Id of the shipping gateway in database</param>
        /// <param name="configurationData">Configuration data as name-value pairs</param>
        void Initialize(int shipGatewayId, Dictionary<String, String> configurationData);

        /// <summary>
        /// Gets the configuration data as name-value pairs
        /// </summary>
        /// <returns>Configuration data as name-value pairs</returns>
        Dictionary<string, string> GetConfigData();

        /// <summary>
        /// Gets a list of services provided by the shipping provider implementation
        /// </summary>
        /// <returns>An array of System.Web.UI.WebControls.ListItem objects containing 
        /// services provided by the shipping provider implementation</returns>
        ListItem[] GetServiceListItems();

        /// <summary>
        /// Gets a shipping rate quote for shipping the given basket items from given warehouse to the given destination address
        /// </summary>
        /// <param name="origin">Warehouse from where shipment initiates</param>
        /// <param name="destination">The destination address for the shipment</param>
        /// <param name="contents">Contents of the shipment</param>
        /// <param name="serviceCode">Service code to get rate quote for</param>
        /// <returns>ShipRateQuote for the given shipping requirements</returns>
        ShipRateQuote GetShipRateQuote(Warehouse origin, Address destination, BasketItemCollection contents, string serviceCode);

        /// <summary>
        /// Gets a shipping rate quote for shipping the given shipment
        /// </summary>
        /// <param name="shipment">The shipment to get rate quote for</param>
        /// <param name="serviceCode">Service code to get rate quote for</param>
        /// <returns>ShipRateQuote for the given shipment</returns>
        ShipRateQuote GetShipRateQuote(IShipment shipment, string serviceCode);


        /// <summary>
        /// Gets tracking summary for a given tracking number
        /// </summary>
        /// <param name="trackingNumber"></param>
        /// <returns></returns>
        TrackingSummary GetTrackingSummary(TrackingNumber trackingNumber);

    }


ShippingProviderBase

To implement a Shipping Provider you can either implement the IShippingProvider interface directly or you can extend the CommerceBuilder.Shipping.Providers.ShippingProviderBase class. ShippingProviderBase class abstracts out the repetitive tasks and provides you some additional utility functions.

The ShippingProviderBase class is defined as follows

    /// <summary>
    /// Base class for shipping provider implementations
    /// </summary>
    public abstract class ShippingProviderBase : IShippingProvider
    {

        #region IShippingProvider Members

        private int _ShipGatewayId;
        public int ShipGatewayId
        {
            get { return _ShipGatewayId; }
            set { _ShipGatewayId = value; }
        }
        private bool _UseDebugMode = false;
        public bool UseDebugMode
        {
            get { return _UseDebugMode; }
            set { _UseDebugMode = value; }
        }

        public abstract string Name { get;}
        public abstract string Version { get;}
        public abstract string Description { get; }

        public abstract string GetLogoUrl(System.Web.UI.ClientScriptManager cs);
        public abstract string GetConfigUrl(System.Web.UI.ClientScriptManager cs);

        public abstract ListItem[] GetServiceListItems();
        public abstract TrackingSummary GetTrackingSummary(TrackingNumber trackingNumber);

        public virtual ShipRateQuote GetShipRateQuote(IShipment shipment, string serviceCode)
        {
            return this.GetShipRateQuote(shipment.Warehouse, shipment.Address, shipment.GetItems(), serviceCode);
        }

        public abstract ShipRateQuote GetShipRateQuote(Warehouse origin, Address destination, BasketItemCollection contents, string serviceCode);


        //Sub classes will usually override this method
        public virtual void Initialize(int shipGatewayId, Dictionary<string, string> configurationData)
        {
            this._ShipGatewayId = shipGatewayId;
            if (configurationData.ContainsKey("UseDebugMode")) UseDebugMode = bool.Parse(configurationData["UseDebugMode"]);
        }

        //Sub classes will usually override this method
        public virtual Dictionary<string, string> GetConfigData()
        {
            Dictionary<string, string> configData = new Dictionary<string, string>();
            configData.Add("UseDebugMode", this.UseDebugMode.ToString());
            return configData;
        }

        #endregion

        /// <summary>
        /// Records communication in App_Data/Logs/ folder
        /// </summary>
        /// <param name="providerName">Name of the provider used for log file name</param>
        /// <param name="direction">Indicates whether this is for data being sent to received from the provider</param>
        /// <param name="message">The message data to record</param>
        protected void RecordCommunication(string providerName, CommunicationDirection direction, string message)
        {
            //GET LOG DIRECTORY
            string directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data\\Logs\\");
            if (!Directory.Exists(directory)) Directory.CreateDirectory(directory);
            string fileName = Path.Combine(directory, providerName + ".Log");
            using (StreamWriter sw = File.AppendText(fileName))
            {
                sw.WriteLine(direction.ToString() + ": " + message);
                sw.WriteLine(string.Empty);
                sw.Close();
            }
        }
        
        /// <summary>
        /// Enumeration that represents the direction of communication between AC and the Gateway/Provider
        /// </summary>
        public enum CommunicationDirection : int 
        { 
            /// <summary>
            /// Data is sent to the gateway
            /// </summary>
            Send, 
            
            /// <summary>
            /// Data is received from the gateway
            /// </summary>
            Receive 
        };

    }


Implementing Required Methods

When you extend ShippingProviderBase class you implement the abstract methods/properties defined in this class. Some of these properties and methods are for information purpose only and easy to implement. For example Name, Description, and Version. Here is an example of how these methods are implemented in FedEx provider.

        public override string Name
        {
            get { return "FedEx"; }
        }

        public override string Version
        {
            get { return "1.0.0"; }
        }

        public override string Description
        {
            get { return "The integrated FedEx module can generate real-time shipping rates for your packages. It also can provide tracking details."; }
        }

GetLogoUrl and GetConfigUrl

Then there are two methods GetLogoUrl and GetConfigUrl that are used to get shipping providers integrated in Ablecommerce merchant console. These methods are called from and ASP.NET page context therefore a ClientScriptManager object is passed as a parameter in these methods to facilitate creation of appropriate URLs.

The GetLogoUrl should return a URL that would give the logo of the provider. This URL can be an external URL or it may be a URL returning the logo from an embedded resource. Here is an example of how this is implemented in FedEx. The logo in FedEx provider is embedded in the DLL.

        public override string GetLogoUrl(ClientScriptManager cs)
        {
            if (cs != null)
                return cs.GetWebResourceUrl(this.GetType(), "CommerceBuilder.Shipping.Providers.FedEx.Logo.gif");
            return string.Empty;
        }

The GetConfigUrl method should return a URL to the page to which control is handed over by Ablecommerce when configuration of this providers is required. This URL is relative to the current directory. Usually new shipping providers are put in their own directory in \Website\Admin\Shipping\Providers folder. Here is an example of how GetConfigUrl method is implemented for FedEx Provider.

        public override string GetConfigUrl(ClientScriptManager cs)
        {
            return "FedEx/Default.aspx";
        }

GetServiceListItems

The GetServiceListItems method is implemented by the shipping providers to tell Ablecommerce the services they are providing. This method returns an array of ListItem objects. Each object has a key and a value. The key is the service code used to uniquely identify a particular service provided by the provider. The value is the display name to be used for that service. Here is an example of how FedEx provider implements GetServiceListItems.

        public override ListItem[] GetServiceListItems()
        {
            List<ListItem> services = new List<ListItem>();
            string value;
            foreach (string key in _services.Keys)
            {
                value = _services[key];
                services.Add(new ListItem(value, key));
            }
            return services.ToArray();
        }

        private static Dictionary<string, string> _services;
        static FedEx()
        {
            _services = new Dictionary<string, string>();
            _services.Add("PRIORITYOVERNIGHT", "FedEx Priority");
            _services.Add("STANDARDOVERNIGHT", "FedEx Standard Overnight");
            _services.Add("FIRSTOVERNIGHT", "FedEx First Overnight");
            _services.Add("FEDEX2DAY", "FedEx 2day");
            _services.Add("FEDEXEXPRESSSAVER", "FedEx Express Saver");
            _services.Add("FEDEX1DAYFREIGHT", "FedEx 1day Freight");
            _services.Add("FEDEX2DAYFREIGHT", "FedEx 2day Freight");
            _services.Add("FEDEX3DAYFREIGHT", "FedEx 3day Freight");
            _services.Add("FEDEXGROUND", "FedEx Ground");
            _services.Add("GROUNDHOMEDELIVERY", "FedEx Ground Home Delivery");
            _services.Add("INTERNATIONALPRIORITY", "FedEx International Priority");
            _services.Add("INTERNATIONALECONOMY", "FedEx International Economy");
            _services.Add("INTERNATIONALFIRST", "FedEx International First");
            _services.Add("INTERNATIONALPRIORITYFREIGHT", "FedEx International Priority Freight");
            _services.Add("INTERNATIONALECONOMYFREIGHT", "FedEx International Economy Freight");
            _services.Add("EUROPEFIRSTINTERNATIONALPRIORITY", "FedEx Euro First International Priority");            
        }

GetShipRateQuote

The most important method where the actual work is done is GetShipRateQuote. This is the method where the provider obtains the shipping rates for the given shipment/basket items for a particular service-code. When implementing shipping providers and getting rates from a remote provider it is not practical to make a call to the remote server for each service code one by one. In practice the implementations get rate quotes for all service codes in one request and cache them for subsequent calls. These rate quotes are cached for the duration of the current request so that subsequent calls to GetShipRateQuote method for the same origin and same destination do not have to connect to the remote server again. Here is how GetShipRateQuote method is implemented in FedEx provider.

        public override ShipRateQuote GetShipRateQuote(Warehouse origin, CommerceBuilder.Users.Address destination, BasketItemCollection contents, string serviceCode)
        {
            Dictionary<string, ProviderShipRateQuote> allQuotes = GetAllServiceQuotes(origin, destination, contents);
            if ((allQuotes != null) && allQuotes.ContainsKey(serviceCode))
            {
                return allQuotes[serviceCode];
            }
            else
            {
                return null;
            }
        }


        private Dictionary<string, ProviderShipRateQuote> GetAllServiceQuotes(Warehouse origin, Address destination, BasketItemCollection contents)
        {            
            string cacheKey = StringHelper.CalculateMD5Hash(Misc.GetClassId(this.GetType()) + "_" + origin.WarehouseId.ToString() + "_" + destination.AddressId.ToString() + "_" + contents.GenerateContentHash());
            HttpContext context = HttpContext.Current;
            if ((context != null) && (context.Items.Contains(cacheKey)))
                return (Dictionary<string, ProviderShipRateQuote>)context.Items[cacheKey];

            //VERIFY WE HAVE A DESTINATION COUNTRY
            if (string.IsNullOrEmpty(destination.CountryCode)) return null;

            PackageList plist = PreparePackages(origin, destination, contents);
            if (plist == null || plist.Count == 0) return null;
            Dictionary<string, ProviderShipRateQuote> allQuotes = new Dictionary<string, ProviderShipRateQuote>();
            List<ProviderShipRateQuote> providerQuotes;
            ProviderShipRateQuote tempQuote;

            foreach (Package item in plist)
            {
                providerQuotes = GetProviderQuotes(origin, destination, item);
                foreach (ProviderShipRateQuote quote in providerQuotes)
                {
                    if (allQuotes.ContainsKey(quote.ServiceCode))
                    {
                        tempQuote = allQuotes[quote.ServiceCode];
                        tempQuote.AddPackageQoute(quote);
                    }
                    else
                    {
                        allQuotes.Add(quote.ServiceCode, quote);
                    }
                }
            }
            
            RemoveInEffectiveQuotes(allQuotes, plist.Count);

            if (context != null) context.Items.Add(cacheKey, allQuotes);
            
            return allQuotes;
        }

Initialize and GetConfigData

Each provider may have its own configuration options and parameters. It is all up to the provider implementation to handle the configuration parameters as needed. Ablecommerce facilitates this by providing two methods in IShippingProvider interface. These are Initialize and GetConfigData. If these two methods are implemented correctly saving and loading of configuration parameters will be handled by Ablecommerce transparently.

The Initialize method is called by Ablecommerce at the time of creation of the shipping provider object. The configuration parameters loaded from the database are passed to this method. In this method the implementation classes should read their custom configuration parameters and initialize themselves accordingly.

The GetConfigData method returns a collection of name-value pairs where each name is the name of a configuration parameter and each value is the value of that configuration parameter. The provider implementations must return all their configuration parameters when this method is called. The parameters returned by this method are saved in the database. When required later these parameters are loaded from the database and passed to Initialize method.

Here is an implementation of Initialize and GetConfigData methods for FedEx Provider

        public override void Initialize(int ShipGatewayId, Dictionary<string, string> ConfigurationData)
        {
            base.Initialize(ShipGatewayId, ConfigurationData);
            //INITIALIZE MY FIELDS
            if (ConfigurationData.ContainsKey("AccountNumber")) AccountNumber = ConfigurationData["AccountNumber"];
            if (ConfigurationData.ContainsKey("MeterNumber")) MeterNumber = ConfigurationData["MeterNumber"];
            if (ConfigurationData.ContainsKey("EnablePackageBreakup")) EnablePackageBreakup = AlwaysConvert.ToBool(ConfigurationData["EnablePackageBreakup"], true);
            if (ConfigurationData.ContainsKey("IncludeDeclaredValue")) IncludeDeclaredValue = AlwaysConvert.ToBool(ConfigurationData["IncludeDeclaredValue"], true);
            if (ConfigurationData.ContainsKey("UseTestMode")) UseTestMode = AlwaysConvert.ToBool(ConfigurationData["UseTestMode"], false);
            if (ConfigurationData.ContainsKey("AccountActive")) AccountActive = AlwaysConvert.ToBool(ConfigurationData["AccountActive"], false);
            if (ConfigurationData.ContainsKey("DropOffType")) DropOffType = (FDXDropOffType)AlwaysConvert.ToEnum(typeof(FDXDropOffType), ConfigurationData["DropOffType"],FDXDropOffType.REGULARPICKUP,true);
            if (ConfigurationData.ContainsKey("PackagingType")) PackagingType = (FDXPackagingType)AlwaysConvert.ToEnum(typeof(FDXPackagingType), ConfigurationData["PackagingType"], FDXPackagingType.YOURPACKAGING, true);
            if (ConfigurationData.ContainsKey("TestModeUrl")) TestModeUrl = ConfigurationData["TestModeUrl"];
            if (ConfigurationData.ContainsKey("LiveModeUrl")) LiveModeUrl = ConfigurationData["LiveModeUrl"];
            if (ConfigurationData.ContainsKey("TrackingUrl")) TrackingUrl = ConfigurationData["TrackingUrl"];
        }

        public override Dictionary<string, string> GetConfigData()
        {
            Dictionary<string, string> configData = base.GetConfigData();
            configData.Add("AccountNumber", this.AccountNumber);
            configData.Add("MeterNumber", this.MeterNumber);
            configData.Add("EnablePackageBreakup", this.EnablePackageBreakup.ToString());
            configData.Add("IncludeDeclaredValue", this.IncludeDeclaredValue.ToString());
            configData.Add("UseTestMode", this.UseTestMode.ToString());
            configData.Add("AccountActive", this.AccountActive.ToString());
            configData.Add("DropOffType", this.DropOffType.ToString());
            configData.Add("PackagingType", this.PackagingType.ToString());
            configData.Add("TestModeUrl", this.TestModeUrl);
            configData.Add("LiveModeUrl", this.LiveModeUrl);
            configData.Add("TrackingUrl", this.TrackingUrl);
            return configData;
        }


Admin Console Pages

An important part of getting a shipping provider implemented is to get it integrated in the merchant admin console. For this purpose you write the ASPX pages to handle provider specific options and configurations. We will not discuss implementation of ASPX pages here. If you want to see examples of how they are implemented you can check implementations in Admin/Shipping/Providers/ folder of your Ablecommerce installation.


Deployment

Once you have completed the implementation code you need to compile your classes and put the resulting DLL in Bin folder of your Ablecommerce installation. Also you need to put your admin console pages in the correct folder under Admin/Shipping/Providers/. The correct folder is the folder that you refer in GetConfigUrl method. Once your DLL is placed in the Bin folder, Ablecommerce will automatically detect your new shipping provider and allow you to add, remove and configure it just like other shipping providers.


Resources