Setting Up USPS Shipping In Spree

by Andrea Singh | May 26, 2010

Integrating shipping via a carrier API (USPS, Fedex, UPS, etc.) with the rails e-commerce platform Spree can be a bit daunting at first. However, once it is understood how the different parts fit together it turns out to be quite easy. The active_shipping extension does all the heavy lifting and it is just a matter of figuring out how to configure it for your particular needs.

In this post I will outline how I set up the active_shipping extension for USPS, but it should be very similar for other carriers.

Shipping Methods, Zones and Calculators

To handle shipping at checkout you need to make one or more shipping methods available. A shipping method has a customer-facing descriptive name, is associated with a particular zone and it uses a calculator. A zone is for the most part a grouping of countries that you pre-define in the admin section. It is used to determine which shipping methods are available for individual orders. A calculator houses the business logic that is responsible for computing the final shipping total.

You can either write your own calculator, enable one of the basic calculators that ship with the Spree gem (for example Calculator::FlatRate) or you can use the calculators provided by an extension, such as the active_shipping extension. The USPS exposes its various delivery services, such as First Class, Media Mail, Priority Mail and so on, through an API. The active_shipping extension uses the active_shipping gem to interface with carrier APIs and ultimately creates usable Spree-type calculators.

For each USPS delivery service you want to offer (e.g. "USPS Media Mail") then, you need a corresponding Shipping Method (set up through the admin panel) and a matching calculator (registered in the active_shipping extension) that ties the delivery service and the shipping method together.

Enabling Shipping Via USPS

Once you've installed the active_shipping extension, you will have several USPS shipping calculators at your disposal that you can now associate with shipping methods. Here is the list of the USPS calculators that the spree-active-shipping extension comes with out of the box:

    
  #in vendor/extensions/active_shipping/active_shipping_extension.rb 
  def activate
    [ 
    #... calculators for Fedex and UPS ...
        Calculator::Usps::MediaMail,
        Calculator::Usps::ExpressMail,
        Calculator::Usps::PriorityMail,
        Calculator::Usps::PriorityMailSmallFlatRateBox,
        Calculator::Usps::PriorityMailRegularMediumFlatRateBoxes,
        Calculator::Usps::PriorityMailLargeFlatRateBox
      ].each(&:register)
  end

Before you can add shipping methods you need to set up zones. I set up two zones, one for domestic shipping - containing only the United States - and a second one for international shipping consisting of all other countries. This setup reflects the basic distinction that is used by the USPS.

With zones in place we can now start adding some shipping methods through the admin panel. The only other essential requirement to calculate the shipping total at checkout is that each product and variant be assigned a weight.

The active_shipping gem needs some configuration variables set in order to consume the carrier web service. Among other things, it needs the API username and the origin location:

  Spree::ActiveShipping::Config.set(:usps_login => "YOUR_USPS_LOGIN")
  Spree::ActiveShipping::Config.set(:origin_country => "US")
  Spree::ActiveShipping::Config.set(:origin_state => "HI")
  Spree::ActiveShipping::Config.set(:origin_city => "Pahoa")
  Spree::ActiveShipping::Config.set(:origin_zip => "96778")
  # these can be set in an initializer in your site extension

Adding Additional USPS Calculators

If the active_shipping extension has not set up a calculator for a particular USPS delivery service that you would like to offer, you can easily add it yourself. For example I needed a First Class domestic and some international calculators, which are not available by default.

The first step is to create a calculator class for each delivery service you want to add, making sure it inherits from Calculator::Usps::Base:

#in vendor/extensions/site/app/models/calculator/usps/first_class_mail_international_parcels.rb
class Calculator::Usps::FirstClassMailInternationalParcels < Calculator::Usps::Base
  def self.description
    "USPS First-Class Mail International Package"
  end
end

Unlike calculators that you write yourself, these additional calculators do not have to implement a #compute instance method that returns a shipping amount, but only need the one description class method, since the superclasses take care of the rest.

However, there is one gotcha to bear in mind. The string returned by the description method must exactly match the name of the USPS delivery service returned by the API call. So how do you find out the exact name of the delivery service? One way to do this is to inspect what gets returned by the API call. Take a look at this excerpt of the code where most of the action takes place:

#vendor/extensions/active_shipping/app/models/calculator/active_shipping.rb  
class Calculator::ActiveShipping < Calculator
  # ...
  def compute(line_items)
   #....  
   rates = retrieve_rates(origin, destination, packages(order))
   # here you can raise the rates hash  

   return nil unless rates
   rate = rates[self.description].to_f + (Spree::ActiveShipping::Config[:handling_fee].to_f || 0.0)
   return nil unless rate
   # divide by 100 since active_shipping rates are expressed as cents

   return rate/100.0
  end  

  def retrieve_rates(origin, destination, packages)
   #....
   # carrier is an instance of ActiveMerchant::Shipping::USPS
   response = carrier.find_rates(origin, destination, packages)
   # turn this beastly array into a nice little hash
   h = Hash[*response.rates.collect { |rate| [rate.service_name, rate.price] }.flatten]
   #....
  end
  # ...
end

Depending on the carrier, in our case USPS, the active_shipping gem returns an array with the name of the service and the price, which the retrieve_rates method converts into a nice hash. The key equals the name of the shipping service and the value is set to the shipping cost. Here is an example of what gets returned for an order with an international destination:

#rates  
{"USPS Priority Mail International Flat Rate Envelope"=>1345, 
"USPS First-Class Mail International Large Envelope"=>376, 
"USPS USPS GXG Envelopes"=>4295, 
"USPS Express Mail International Flat Rate Envelope"=>2895, 
"USPS First-Class Mail International Package"=>396, 
"USPS Priority Mail International Medium Flat Rate Box"=>4345, 
"USPS Priority Mail International"=>2800, 
"USPS Priority Mail International Large Flat Rate Box"=>5595, 
"USPS Global Express Guaranteed Non-Document Non-Rectangular"=>4295, 
"USPS Global Express Guaranteed Non-Document Rectangular"=>4295, 
"USPS Global Express Guaranteed (GXG)"=>4295, 
"USPS Express Mail International"=>2895, 
"USPS Priority Mail International Small Flat Rate Box"=>1345}

From all the possible shipping services, only the one that matches the description of the calculator gets selected. At this point an optional flat handling fee (set via preferences) can be added:

rate = rates[self.description].to_f + (Spree::ActiveShipping::Config[:handling_fee].to_f || 0.0) 

Finally, don't forget to register the new calculators you added. In extensions, this is accomplished in the activate method:

# vendor/extensions/site_extension.rb 
def activate
  [ Calculator::Usps::FirstClassMailInternationalParcels,
    Calculator::Usps::PriorityMailInternational,
    Calculator::Usps::FirstClassMailParcels
  ].each(&:register)
end

Changing Availability of Shipping Methods on Criteria Other Than the Zone

Ordinarily it is the zone of the shipping address that determines which shipping methods are displayed to a customer at checkout. Here is how is the availability of a shipping method is determined:

  class Checkout < ActiveRecord::Base
    #...
    def shipping_methods 
      return [] unless ship_address
      ShippingMethod.all_available(order)
    end
    #...
  end

  class ShippingMethod < ActiveRecord::Base
  #.....
    def available?(order)
      calculator.available?(order)
    end

    def available_to_order?(order)
      available?(order) &amp;&amp; zone &amp;&amp; zone.include?(order.ship_address)
    end

    def self.all_available(order)
      all.select { |method| method.available_to_order?(order)}
    end
  end

Unless overridden, the calculator's #available? method returns true by default. It is the zone of the destination address that filters out the shipping methods. However, in some circumstances it may be necessary to filter out additional shipping methods.

For example, I needed to remove the First Class domestic shipping service for orders where the combined shipping weight was over 13oz, since First Class is only available up to 13oz. Even though the USPS API does not return First Class in its array of options, nonetheless First Class will appear as an option in the checkout view with a value of 0, since it has been set as a Shipping Method.

In order to remove the First Class shipping method from orders that weigh more than 13oz, I needed to override the calculator's #available? method:

  class Calculator::Usps::FirstClassMailParcels < Calculator::Usps::Base
    def self.description
      "USPS First-Class Mail Parcel"
    end

    def available?(order)
      multiplier = Spree::ActiveShipping::Config[:unit_multiplier]
      weight = order.line_items.inject(0) do |weight, line_item|
        weight + (line_item.variant.weight ? (line_item.quantity * line_item.variant.weight * multiplier) : 0)
      end
      #if weight in ounces > 13, then First Class Mail is not available for the order
      weight > 13 ? false : true
    end
  end