• Home

  • Custom Ecommerce
  • Application Development
  • Database Consulting
  • Cloud Hosting
  • Systems Integration
  • Legacy Business Systems
  • Security & Compliance
  • GIS

  • Expertise

  • About Us
  • Our Team
  • Clients
  • Blog
  • Careers

  • CasePointer

  • VisionPort

  • Contact
  • Our Blog

    Ongoing observations by End Point Dev people

    Integrating the Estes Freight Shipping SOAP API as a Spree Shipping Calculator

    Patrick Lewis

    By Patrick Lewis
    September 28, 2021

    Cargo ship on sea with dark clouds

    One of our clients with a Spree-based e-commerce site was interested in providing automated shipping quotes to their customers using their freight carrier Estes. After doing some research I found that Estes provided a variety of SOAP APIs and determined a method for extending Spree with custom shipping rate calculators. This presented an interesting challenge to me on several levels: most of my previous API integration experience was with REST, not SOAP APIs, and I had not previously worked on custom shipping calculators for Spree. Fortunately, the Estes SOAP API documentation and some code examples of other Spree shipping calculators were all I needed to create a successful integration of the freight shipping API for this client.

    Estes API Documentation

    The Estes Rate Quote Web Service API is the one that I relied on for being able to generate shipping quotes based on a combination of source address, destination address, and package weight. I found the developer documentation to be thorough and helpful, and was able to create working client code to send a request and receive a response relatively quickly. Many optional fields can be provided when making requests but I found that I only needed to use a small subset of these, as shown in the example code below.

    The one aspect of the API that tripped me up a bit was their use of CN as the country code for Canada; Spree and most other codebases I have encountered use the international standard ISO 3166 country codes with CA for Canada, so I had to add a small workaround for that in my client code when requesting shipping quotes to Canadian addresses. Another limitation I encountered is that the API expected to receive only 5-digit US and 6-character Canadian postal codes, so I had to do a bit of manipulation in my shipping calculator to account for that.

    Ruby SOAP Client

    I researched Ruby SOAP clients and soon found Savon, the “Heavy metal SOAP client”, which proved to be very easy to integrate into my existing Rails/​Spree application. I added the savon gem to my project’s Gemfile and I was quickly able to instantiate a Savon client and configure it using the WSDL provided by Estes. After that, most of the integration work involved crafting a valid XML payload for my request and then parsing the response.

    Spree Shipping Calculators

    The final piece of the puzzle was implementing the Savon client into a Spree shipping calculator class. This process allowed me to retrieve details about the current user’s order and then return the calculated shipping estimates within the context of the Spree checkout pages. Looking at existing shipping calculator code helped set me on the right path here; in the end, it was just a matter of defining a new class that inherited from the base Spree::ShippingCalculator class and then defining the #compute_package method expected by Spree for returning the shipping cost of a given package.

    Code Example

    # app/models/spree/calculator/shipping/estes_calculator.rb
    module Spree::Calculator::Shipping
      # Custom freight shipping rate API integration
      class EstesCalculator < Spree::ShippingCalculator
        ESTES_API_URL = 'https://www.estes-express.com/tools/rating/ratequote/v4.0/services/RateQuoteService?wsdl'.freeze
    
        def self.description
          'Estes Freight'
        end
    
        def compute_package(package)
          country_code = package.order.ship_address.country.iso
          return 0 unless country_code.in?(['US', 'CA', 'MX'])
    
          client = Savon.client(
            filters: %i[user password account],
            log: true,
            log_level: :debug,
            logger: Logger.new(Rails.root.join('log', 'savon.log')),
            pretty_print_xml: true,
            wsdl: ESTES_API_URL
          )
    
          country_code = 'CN' if country_code == 'CA' # Estes uses CN for Canada
          postal_code = package.order.ship_address.zipcode
    
          postal_code =
            if country_code == 'CN'
              postal_code.delete(' ').first(6)
            else
              postal_code.first(5)
            end
    
          xml = <<~XML
            <soapenv:Envelope
                xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                xmlns:rat="http://ws.estesexpress.com/ratequote"
                xmlns:rat1="http://ws.estesexpress.com/schema/2019/01/ratequote">
              <soapenv:Header>
                <rat:auth>
                  <rat:user>#{ENV['ESTES_USER']}</rat:user>
                  <rat:password>#{ENV['ESTES_PASSWORD']}</rat:password>
                </rat:auth>
              </soapenv:Header>
              <soapenv:Body>
                <rat1:rateRequest>
                  <rat1:requestID>#{package.order.number}</rat1:requestID>
                  <rat1:account>#{ENV['ESTES_ACCOUNT']}</rat1:account>
                  <rat1:originPoint>
                    <rat1:countryCode>US</rat1:countryCode>
                    <rat1:postalCode>10001</rat1:postalCode>
                  </rat1:originPoint>
                  <rat1:destinationPoint>
                    <rat1:countryCode>#{country_code}</rat1:countryCode>
                    <rat1:postalCode>#{postal_code}</rat1:postalCode>
                  </rat1:destinationPoint>
                  <rat1:payor>S</rat1:payor>
                  <rat1:terms>C</rat1:terms>
                  <rat1:baseCommodities>
                    <rat1:commodity>
                      <rat1:class>50</rat1:class>
                      <rat1:weight>#{package_weight(package)}</rat1:weight>
                    </rat1:commodity>
                  </rat1:baseCommodities>
                </rat1:rateRequest>
              </soapenv:Body>
            </soapenv:Envelope>
          XML
    
          response = client.call(:get_quote, xml: xml)
          quotes = response.body.dig(:rate_quote, :quote_info, :quote)
    
          if quotes.is_a?(Array)
            quotes.first.dig(:pricing, :total_price).to_f
          elsif quotes.is_a?(Hash)
            quotes.dig(:pricing, :total_price).to_f
          else
            0
          end
        rescue Savon::Error
          # Record shipping rate as 0 if an API error is caught, 0 amount will indicate need to show user an error message on the shipping rate page
          0
        end
    
        private
    
        def package_weight(package)
          weight = package.contents.sum { |content| content.line_item.weight }
    
          if weight < 5 # enforce minimum package weight
            5
          else
            weight.round
          end
        end
      end
    end
    

    Conclusion

    I was pleased that I was able to quickly build a custom shipping calculator in Spree that used the Estes Rate Quote API to provide accurate shipping estimates for large freight packages. The high quality documentation of the Estes API and the Savon SOAP Client gem made for a pleasant development experience, and the client was happy to gain the new functionality for their Spree store.

    api ruby rails shipping spree


    Comments