Get in touch Contact icon Contact icon Contact icon Request a demo

Digital Commerce Insights

Preparing Brands for Exponential Growth

Get FedEx Account Rates Using Arc.js

By Tay Moore | June 28, 2016

Categories: Mozu Developers & Users

Get FedEx Account Rates Using Arc.js

Arc.js is a powerful tool that helps developers build a more robust shopper experience. Arc.js opens up the Mozu API in such a way that you can bind actions on API calls and alter the commerce model before it is sent to the front end.

In this blog, you learn how to use Arc.js to obtain account rates from the FedEx API and apply them to the shipping model. This is useful if you have negotiated special rates with FedEx and wish to use them when the customer is checking out.

Prerequisites

  1. Before you begin, complete the Arc.js quick start guide to get an overview of how Arc.js works. As part of the guide, you will install the required software needed to proceed through this blog, which includes Node.js and a Yeoman generator that scaffolds Arc.js actions.
  2. After completing the guide, create a new application in Dev Center to house the Arc.js project described in this blog.

The Action

Arc.js runs off of actions. These actions run every time an API call is made. For this instance, we are interested in the action that runs after the GetRates API operation completes in Mozu. The name of the action is http.commerce.catalog.storefront.shipping.requestRates.after.

To get started, scaffold the action in your Arc.js project:

  1. Open a command prompt in your project root directory and enter <code>yo mozu-actions</code> to run the Mozu Yeoman Generator (if you prefer to add the action to an existing Arc.js project instead of starting from scratch, enter <code>yo mozu-actions:action</code> to add the action to an existing project).
  2. After specifying your project details, select the <code>commerce.catalog.storefront.shipping</code> domain and hit the spacebar.
  3. Select <code>http.commerce.catalog.storefront.shipping.requestRates.after</code> and hit the space bar.

After you have finished creating the action, you are ready to write the code.

Building the Code

SOAP Setup

To get the FedEx account rates, you must make a call to FedEx’s API. Their API uses SOAP, so we’ll install the SOAP package first using npm. We also suggest you install the extend package and the lodash packages.

  1. Open a command prompt and enter <code>npm install soap</code>.
  2. Enter <code>npm install extend</code>.
  3. Enter <code>npm install -save lodash</code>.

Before you can call the FedEx API, SOAP requires reading a WSDL file (inside of <code>lib/WSDL.js</code>, on line 2115, the package attempts to open a WSDL file, if it’s not a URL with the function <code>fs.readFile()</code>). However, Arc.js cannot read directly from files. To get around this issue:

  1. Create a file in the <code>assets\src\domains\commerce.catalog.storefront.shipping</code> directory and save it as <code>soap-init.js</code>.
  2. Enter the following code in the <code>soap-init.js</code> file:

3. In the <code>assets\src\domains\commerce.catalog.storefront.shipping\ http.commerce.catalog.storefront.shipping.requestRates.after.js</code> file, enter the following code:

var SoapInit = require("./soap-init");

var _ = require("lodash");



module.exports = function(context, callback) {

    var soapinit = SoapInit;

    var authentication = Authentication;

    var soap = require('soap');

    callback();
};

Now that you have the SOAP package setup, the next step is to prepare the data for transfer.

FedEx Information

To make the API call, you must send data to FedEx. To do this, you need to create a JSON object that will be packaged into the SOAP call. The JSON object needs to look similar to:

WebAuthenticationDetail: {

    UserCredential: {

        Key: "55555555555",

        Password: "***********"

    }

},

ClientDetail: {

    AccountNumber: "55555555555",

    MeterNumber: "55555555555"

}
,
Version = {

    ServiceId: 'crs',

    Major: 16,

    Intermediate: 0,

    Minor: 0
},
ReturnTransitAndCommit: true,
CarrierCodes: ['FDXE','FDXG'],

RequestedShipment: {
    DropoffType: 'REGULAR_PICKUP',

    PackagingType: 'YOUR_PACKAGING',
    Shipper: {
        Contact: {

            PersonName: 'Sender Name',
            CompanyName: 'Company Name',

            PhoneNumber: '5555555555'
        },
        Address: {
            StreetLines: [
"555 Congress Road"],
            City: "Austin",

            StateOrProvinceCode: "TX",
            PostalCode: "78758",
            CountryCode: "US"
        }

    },
   
    Recipient: {
        Contact: {
            PersonName: 'Recipient Name',
            CompanyName: 'Company Receipt Name',

            PhoneNumber: '5555555555'

        },
        Address: {

            StreetLines: [
"1234 Memorial Drive"],

            City: "Austin",

            StateOrProvinceCode: "TX",

            PostalCode: "78758",
            CountryCode: "US",
            Residential: true
        }

    },
    ShippingChargesPayment: {
        PaymentType: 'SENDER',
        Payor: {

            ResponsibleParty: {

                AccountNumber: "5555555555555"
            }
        }
    },

    PackageCount: '1',
    RequestedPackageLineItems: {

        SequenceNumber: 1,
        GroupPackageCount: 1,

        Weight: {
            Units: 'LB',
            Value: "10"
        },

        Dimensions: {
            Length: "4",

            Width: "6",
            Height: "10",

            Units: "IN"
        }

    }

}

To create this JSON object, you leverage the help of a custom function.

  1. Create a file in the <code>\assets\src\domains\commerce.catalog.storefront.shipping</code> directory and save it as <code>fedexParams.js</code>.
  2. Code this new file as follows:
var extend = require("extend");


module.exports = function generateAuthentication(cred, shippingInfo) {

        var params = {

            WebAuthenticationDetail: {

                UserCredential: {

                    Key: cred.Key,

                    Password: cred.Password

                }

            },

            ClientDetail: {

                AccountNumber: cred.AccountNumber,

                MeterNumber: cred.MeterNumber

            }

        };


        params.Version = {

            ServiceId: 'crs',

            Major: 16,

            Intermediate: 0,

            Minor: 0

        };


        return extend(params, {

            ReturnTransitAndCommit: true,

            CarrierCodes: ['FDXE','FDXG'],

            RequestedShipment: {

                DropoffType: 'REGULAR_PICKUP',

                PackagingType: 'YOUR_PACKAGING',

                Shipper: {

                    Contact: {

                        PersonName: 'Sender Name',

                        CompanyName: 'Company Name',

                        PhoneNumber: '5555555555'

                    },

                    Address: {

                        StreetLines: [
 shippingInfo.OriginAddress.Address1
 ],

                        City: shippingInfo.OriginAddress.CityOrTown,

                        StateOrProvinceCode: shippingInfo.OriginAddress.StateOrProvince,

                        PostalCode: shippingInfo.OriginAddress.PostalOrZipCode,

                        CountryCode: shippingInfo.OriginAddress.CountryCode

                    }

                },

                Recipient: {

                    Contact: {

                        PersonName: 'Recipient Name',

                        CompanyName: 'Company Receipt Name',

                        PhoneNumber: '5555555555'

                    },

                    Address: {

                        StreetLines: [ shippingInfo.DestinationAddress.Address1 ],

                        City: shippingInfo.DestinationAddress.CityOrTown,

                        StateOrProvinceCode: shippingInfo.DestinationAddress.StateOrProvince.length <= 2 ? shippingInfo.DestinationAddress.StateOrProvince : "",

                        PostalCode: shippingInfo.DestinationAddress.PostalOrZipCode,

                        CountryCode: shippingInfo.DestinationAddress.CountryCode,

                        Residential: shippingInfo.DestinationAddress.AddressType == "Residential" ? true : false

                    }

                },

                ShippingChargesPayment: {

                    PaymentType: 'SENDER',

                    Payor: {

                        ResponsibleParty: {

                            AccountNumber: cred.AccountNumber

                        }

                    }

                },

                PackageCount: '1',

                RequestedPackageLineItems: {

                    SequenceNumber: 1,

                    GroupPackageCount: 1,

                    Weight: {

                        Units: shippingInfo.Items[0].UnitMeasurements.Weight.Unit == "lbs" ? 'LB' : shippingInfo.Items[0].UnitMeasurements.Weight.Unit,

                        Value: shippingInfo.Items[0].UnitMeasurements.Weight.Value

                    },

                    Dimensions: {

                        Length: shippingInfo.Items[0].UnitMeasurements.Length.Value,

                        Width: shippingInfo.Items[0].UnitMeasurements.Width.Value,

                        Height: shippingInfo.Items[0].UnitMeasurements.Height.Value,

                        Units: shippingInfo.Items[0].UnitMeasurements.Length.Unit.toUpperCase()

                    }

                }

            }

        });

};

This function uses the FedEx credentials (API key, password, account number, and meter number) and the data passed from the shipping model (context.request.body). It also includes logic that handles the FedEx API, which is very particular about the information that is sent to it.

3. Update the <code>assets\src\domains\commerce.catalog.storefront.shipping\ http.commerce.catalog.storefront.shipping.requestRates.after.js</code> file to look like:

var SoapInit = require("./soap-init");

var Authentication = require("./generateAuthentication");

var _ = require("lodash");



module.exports = function(context, callback) {

    var soapinit = SoapInit;

    var authentication = Authentication;

    var soap = require('soap');

    // FedEx credentials

    var cred = {

        Key: 'FedEx_Key',

        Password: 'FedEx_Password',

        AccountNumber: 'FedEx_Account#',

        MeterNumber: 'FedEx_Meter#'

    };


    var fedexParams = authentication(cred, context.request.body);


};

After you’ve set this function up, you are ready to write the SOAP request.

Making the Call

  1. Update the <code>assets\src\domains\commerce.catalog.storefront.shipping\ http.commerce.catalog.storefront.shipping.requestRates.after.js</code> file to look similar to:
var SoapInit = require("./soap-init");

var Authentication = require("./generateAuthentication");

var _ = require("lodash");



module.exports = function(context, callback) {

    var soapinit = SoapInit;

    var authentication = Authentication;

    var soap = require('soap');

    // FedEx credentials

    var cred = {

        Key: 'FedEx_Key',

        Password: 'FedEx_Password',

        AccountNumber: 'FedEx_Account#',

        MeterNumber: 'FedEx_Meter#'

    };

    var fedexParams = authentication(cred, context.request.body);

    
// Create SOAP client to send data to FedEx API

    soap.createClient("./RateService_v16.wsdl", {endpoint: "https://ws.fedex.com/web-services"}, function(err, client){


        // Send data to FedEx API

        client.getRates(fedexParams, function(err, result){

        });
    });
};

Code Comments:

To call the FedEx API, you must first make the client. The preceding code calls the createClient method from the SOAP package. To make the client, you need the WSDL and an endpoint. It looks like:

soap.createClient("./RateService_v16.wsdl", {endpoint: "https://ws.fedex.com/web-services"}, function(err, client){});

Inside that function callback will be your client. That client has all the methods that were defined inside of the WSDL. The method that we care about is <code>getRates</code>. Now that you have the client, you can make a FedEx API call using your JSON object that you made earlier. To make this call, you will invoke:

client.getRates(fedexParams, function(err, result){});

Again, inside that function will be a callback with your results. The object will have information such as if the request was a success and, more importantly, it will include the shipping rate details.

Altering the Model

  1. Update the <code>assets\src\domains\commerce.catalog.storefront.shipping\ http.commerce.catalog.storefront.shipping.requestRates.after.js</code> file to look similar to:
var SoapInit = require("./soap-init");

var Authentication = require("./generateAuthentication");

var _ = require("lodash");



module.exports = function(context, callback) {

    var soapinit = SoapInit;

    var authentication = Authentication;

    var soap = require('soap');

    // FedEx credentials

    var cred = {

        Key: 'FedEx_Key',

        Password: 'FedEx_Password',

        AccountNumber: 'FedEx_Account#',

        MeterNumber: 'FedEx_Meter#'

    };

    var fedexParams = authentication(cred, context.request.body);

    
// Create SOAP client to send data to FedEx API

    soap.createClient("./RateService_v16.wsdl", {endpoint: "https://ws.fedex.com/web-services"}, function(err, client){


        // Send data to FedEx API

        client.getRates(fedexParams, function(err, result){

                
            var fedexRates = result.RateReplyDetails;

            // Get index of Mozu's FedEx shipping information

            var mozuIndex = _.findIndex(context.response.body.rates, function(rate){

                return rate.carrierId == "fedex";

            });
            if(mozuIndex > -1){
                // Mozu's FedEx information

                var shippingRates = context.response.body.rates[mozuIndex].shippingRates;
                // Loop through each rate returned from fedex, and change the Mozu listed rates to the FedEx account rates

                
_.forEach(fedexRates, function(rate){
                    // Check if rate is enabled

                    var index = _.findIndex(shippingRates, function(info){
                        return info.code == "fedex_"+ rate.ServiceType;
                    });
                    // if rate is enabled, change Mozu model
                    if(index > -1){

                    } else if (rate.ServiceType == "GROUND_HOME_DELIVERY"){
                        index = _.findIndex(shippingRates, function(info){
                            return info.code == "fedex_FEDEX_GROUND";

                        });
                    }
            }
        });
    });
};

Code Comments

Now that we’ve gotten a response from FedEx, we need to apply those shipping rates to our Mozu shipping model. Before you begin applying the new rates to the shipping model, check to see if FedEx is enabled. To do this, we can use lodash to check the shipping model for FedEx:

var mozuIndex = _.findIndex(context.response.body.rates, function(rate){
                    return rate.carrierId == "fedex";
                });

Now you can check to see if that number is greater than -1, if so then there is some FedEx shipping rates for us to change. We can also use this information to alter the FedEx shipping rates that is returned from the action:

var shippingRates = context.response.body.rates[mozuIndex].shippingRates;

You should also make a variable for the newly acquired account rates from FedEx, so it’s easier to call:

var fedexRates = result.RateReplyDetails;

Now you are ready to sort through the model and apply your newly gathered FedEx account rates. To do this, you can use lodash to go through each item returned from the SOAP response and check if there is a comparable FedEx method in the shipping model. All of the FedEx methods in the shipping model has a code attribute that follows the naming scheme of FedEx_{{ name of service type }}. So if the SOAP response has a rate with a service type 2_DAY_PRIORITY, the FedEx method in the shipping model will have a code of fedex_2_DAY_PRIORITY. Using this you can find correlation between the shipping model and the response from FedEx. The only exception to this naming scheme is the ground rates. I added an extra bit of logic to deal with ground rates.

2. Almost there! Update the <code>assets\src\domains\commerce.catalog.storefront.shipping\ http.commerce.catalog.storefront.shipping.requestRates.after.js</code> file to look similar to:

shippingRates[index].amount = parseFloat(rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalNetChargeWithDutiesAndTaxes.Amount);

_.forEach(shippingRates[index].customAttributes, function(attr){

    if(attr.key == "base_charge"){
        attr.value = rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalBaseCharge.Amount +" USD";
    } else if (attr.key == "total_freight_discounts"){

        attr.value = rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalFreightDiscounts.Amount +" USD";
    } else if (attr.key == "total_surcharges"){
        attr.value = rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalSurcharges.Amount +" USD";

    }

});

shippingRates[index].shippingItemRates[0].amount = parseFloat(rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalNetChargeWithDutiesAndTaxes.Amount);

Code Comments

The last step to adding the new rates to our shipping model, is to change the data in the model. Now that we have a way to associate the shipping model with what is returned from the SOAP call, we can start to alter the model in a meaningful way. There are only two items that needs to be altered in the shipping model, <code>amount</code> and <code>shippingItems[0].amount</code>. But we can take it a step farther and add the base charge, total freight discounts, and total surcharges. Those three are not needed in the model unless you use it in your theme, but it wouldn’t hurt to add it if you decide in the future you would need those attributes. Also, it is important to note that the shipping model expects these numbers to be float numbers, but the SOAP response returns the numbers in string. You can use <code>parseFloat()</code> to change the data type. So you should add code like this to alter the shipping model.

The Completed Code

If you stuck with us up to this point, your code should look similar to what’s below. This code includes comments and error handling in the event that the SOAP call does not work as expected.

var SoapInit = require("./soap-init");

var Authentication = require("./generateAuthentication");

var _ = require("lodash");


module.exports = function(context, callback) {

    var soapinit = SoapInit;

    var authentication = Authentication;

    var soap = require('soap');


    
// FedEx credentials

    var cred = {

        Key: 'FedEx_Key',

        Password: 'FedEx_Password',

        AccountNumber: 'FedEx_Account#',

        MeterNumber: 'FedEx_Meter#'

    };

    // Create SOAP client to send data to FedEx API

    soap.createClient("./RateService_v16.wsdl", {endpoint: "https://ws.fedex.com/web-services"}, function(err, client){


        var fedexParams = authentication(cred, context.request.body);

        // Send data to FedEx API

        client.getRates(fedexParams, function(err, result){


            // Check if returned FedEx results are successful

            if(result.HighestSeverity == 'SUCCESS'){

                var fedexRates = result.RateReplyDetails;


                
// Get index of Mozu's FedEx shipping information

                var mozuIndex = _.findIndex(context.response.body.rates, function(rate){

                    return rate.carrierId == "fedex";

                });

                if(mozuIndex > -1){

                // Mozu's FedEx information

                    var shippingRates = context.response.body.rates[mozuIndex].shippingRates;


                    
// Loop through each rate returned from fedex, and change the Mozu listed rates to the FedEx account rates

                
    _.forEach(fedexRates, function(rate){

                        // Check if rate is enabled

                        var index = _.findIndex(shippingRates, function(info){

                            return info.code == "fedex_"+ rate.ServiceType;

                        });

                        
// if rate is enabled, change Mozu model

                        if(index > -1){

                            shippingRates[index].amount = parseFloat(rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalNetChargeWithDutiesAndTaxes.Amount);

                            _.forEach(shippingRates[index].customAttributes, function(attr){

                                if(attr.key == "base_charge"){

                                    attr.value = rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalBaseCharge.Amount +" USD";

                                } else if (attr.key == "total_freight_discounts"){

                                    attr.value = rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalFreightDiscounts.Amount +" USD";

                                } else if (attr.key == "total_surcharges"){

                                    attr.value = rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalSurcharges.Amount +" USD";

                                }

                            });

                            shippingRates[index].shippingItemRates[0].amount = parseFloat(rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalNetChargeWithDutiesAndTaxes.Amount);

                        } else if (rate.ServiceType == "GROUND_HOME_DELIVERY"){


                            // Ground is unique and has a different code

                            index = _.findIndex(shippingRates, function(info){

                                return info.code == "fedex_FEDEX_GROUND";

                            });

                            shippingRates[index].amount = parseFloat(rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalNetChargeWithDutiesAndTaxes.Amount);

                            _.forEach(shippingRates[index].customAttributes, function(attr){

                                if(attr.key == "base_charge"){

                                    attr.value = rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalBaseCharge.Amount +" USD";

                                } else if (attr.key == "total_freight_discounts"){

                                    attr.value = rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalFreightDiscounts.Amount +" USD";

                                } else if (attr.key == "total_surcharges"){

                                    attr.value = rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalSurcharges.Amount +" USD";

                                }

                            });

                            shippingRates[index].shippingItemRates[0].amount = parseFloat(rate.RatedShipmentDetails[0].ShipmentRateDetail.TotalNetChargeWithDutiesAndTaxes.Amount);

                        }

                    });

                }

            } else {

                console.log(result);

            }

            callback();
        });

    });

};

GitHub Download

You can access a GitHub repo with the code discussed in this blog. It has a simple walkthrough of how to clone the repo and setup the code with your tenant.


Leave a reply