Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add support for Shipwire Shipping Rate API

  • Loading branch information...
commit 52775bea66c913e9f038f808640c647681bcf72e 1 parent 203949f
Cody Fauser authored
View
1  CHANGELOG
@@ -1,3 +1,4 @@
+* Add support for Shipwire Shipping Rate API [cody]
* Cleanup package tests [cody]
* Remove unused Carrier#setup method [cody]
* Don't use Array splat in Regex comparisons in Package [cody]
View
1  lib/active_shipping/shipping/carriers.rb
@@ -2,6 +2,7 @@
require 'active_shipping/shipping/carriers/ups'
require 'active_shipping/shipping/carriers/usps'
require 'active_shipping/shipping/carriers/fedex'
+require 'active_shipping/shipping/carriers/shipwire'
module ActiveMerchant
module Shipping
View
154 lib/active_shipping/shipping/carriers/shipwire.rb
@@ -0,0 +1,154 @@
+require 'cgi'
+require 'builder'
+
+module ActiveMerchant
+ module Shipping
+ class Shipwire < Carrier
+ URL = 'https://www.shipwire.com/exec/RateServices.php'
+ SCHEMA_URL = 'http://www.shipwire.com/exec/download/RateRequest.dtd'
+ WAREHOUSES = { 'CHI' => 'Chicago',
+ 'LAX' => 'Los Angeles',
+ 'REN' => 'Reno',
+ 'VAN' => 'Vancouver',
+ 'TOR' => 'Toronto',
+ 'UK' => 'United Kingdom'
+ }
+
+ CARRIERS = [ "UPS", "USPS", "FedEx", "Royal Mail", "Parcelforce", "Pharos", "Eurotrux", "Canada Post", "DHL" ]
+
+ SUCCESS = "OK"
+ SUCCESS_MESSAGE = "Successfully received the shipping rates"
+ NO_RATES_MESSAGE = "No shipping rates could be found for the destination address"
+ REQUIRED_OPTIONS = [:login, :password].freeze
+
+ def find_rates(origin, destination, packages, options = {})
+ requires!(options, :items)
+ commit(origin, destination, options)
+ end
+
+ private
+ def requirements
+ REQUIRED_OPTIONS
+ end
+
+ def build_request(destination, options)
+ xml = Builder::XmlMarkup.new
+ xml.instruct!
+ xml.declare! :DOCTYPE, :RateRequest, :SYSTEM, SCHEMA_URL
+ xml.tag! 'RateRequest' do
+ add_credentials(xml)
+ add_order(xml, destination, options)
+ end
+ xml.target!
+ end
+
+ def add_credentials(xml)
+ xml.tag! 'EmailAddress', @options[:login]
+ xml.tag! 'Password', @options[:password]
+ end
+
+ def add_order(xml, destination, options)
+ xml.tag! 'Order', :id => options[:order_id] do
+ xml.tag! 'Warehouse', options[:warehouse] || '00'
+
+ add_address(xml, destination)
+ Array(options[:items]).each_with_index do |line_item, index|
+ add_item(xml, line_item, index)
+ end
+ end
+ end
+
+ def add_address(xml, destination)
+ xml.tag! 'AddressInfo', :type => 'Ship' do
+ xml.tag! 'Address1', destination.address1
+ xml.tag! 'Address2', destination.address2 unless destination.address2.blank?
+ xml.tag! 'Address3', destination.address3 unless destination.address3.blank?
+ xml.tag! 'City', destination.city
+ xml.tag! 'State', destination.state unless destination.state.blank?
+ xml.tag! 'Country', destination.country_code
+ xml.tag! 'Zip', destination.zip unless destination.zip.blank?
+ end
+ end
+
+ # Code is limited to 12 characters
+ def add_item(xml, item, index)
+ xml.tag! 'Item', :num => index do
+ xml.tag! 'Code', item[:sku]
+ xml.tag! 'Quantity', item[:quantity]
+ end
+ end
+
+ def commit(origin, destination, options)
+ request = build_request(destination, options)
+ save_request(request)
+
+ response = parse( ssl_post(URL, "RateRequestXML=#{CGI.escape(request)}") )
+
+ RateResponse.new(response["success"], response["message"], response,
+ :xml => response,
+ :rates => build_rate_estimates(response, origin, destination),
+ :request => last_request,
+ :log_xml => options[:log_xml]
+ )
+ end
+
+ def build_rate_estimates(response, origin, destination)
+ response["rates"].collect do |quote|
+ RateEstimate.new(origin, destination, carrier_for(quote["service"]), quote["service"],
+ :service_code => quote["method"],
+ :total_price => quote["cost"],
+ :currency => quote["currency"]
+ )
+ end
+ end
+
+ def carrier_for(service)
+ CARRIERS.dup.find{ |carrier| service.to_s =~ /^#{carrier}/i } || service.to_s.split(" ").first
+ end
+
+ def parse(xml)
+ response = {}
+ response["rates"] = []
+
+ document = REXML::Document.new(xml)
+
+ response["status"] = parse_child_text(document.root, "Status")
+
+ document.root.elements.each("Order/Quotes/Quote") do |e|
+ rate = {}
+ rate["method"] = e.attributes["method"]
+ rate["warehouse"] = parse_child_text(e, "Warehouse")
+ rate["service"] = parse_child_text(e, "Service")
+ rate["cost"] = parse_child_text(e, "Cost")
+ rate["currency"] = parse_child_attribute(e, "Cost", "currency")
+ response["rates"] << rate
+ end
+
+ if response["status"] == SUCCESS && response["rates"].any?
+ response["success"] = true
+ response["message"] = SUCCESS_MESSAGE
+ elsif response["status"] == SUCCESS && response["rates"].empty?
+ response["success"] = false
+ response["message"] = NO_RATES_MESSAGE
+ else
+ response["success"] = false
+ response["message"] = parse_child_text(document.root, "ErrorMessage")
+ end
+
+ response
+ end
+
+ def parse_child_text(parent, name)
+ if element = parent.elements[name]
+ element.text
+ end
+ end
+
+ def parse_child_attribute(parent, name, attribute)
+ if element = parent.elements[name]
+ element.attributes[attribute]
+ end
+ end
+ end
+ end
+end
View
5 test/fixtures.yml
@@ -7,4 +7,7 @@ ups:
fedex:
login: '510087666'
password: '1250153'
- test: true
+ test: true
+shipwire:
+ login:
+ password:
View
17 test/fixtures/xml/shipwire/international_rates_response.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<RateResponse>
+ <Status>OK</Status>
+ <Order sequence="1">
+ <Quotes>
+ <Quote method="INTL">
+ <Warehouse>UK</Warehouse>
+ <Service>UPS Standard</Service>
+ <Cost currency="USD">28.06</Cost>
+ <DeliveryEstimate>
+ <Minimum units="days">1</Minimum>
+ <Maximum units="days">5</Maximum>
+ </DeliveryEstimate>
+ </Quote>
+ </Quotes>
+ </Order>
+</RateResponse>
View
4 test/fixtures/xml/shipwire/invalid_credentials_response.xml
@@ -0,0 +1,4 @@
+<RateResponse>
+ <Status>Error</Status>
+ <ErrorMessage><![CDATA[Could not verify e-mail/password combination]]></ErrorMessage>
+</RateResponse>
View
18 test/fixtures/xml/shipwire/new_carrier_rate_response.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE RateResponse SYSTEM "http://www.shipwire.com/exec/download/RateResponse.dtd">
+<RateResponse>
+ <Status>OK</Status>
+ <Order sequence="1">
+ <Quotes>
+ <Quote method="GD">
+ <Warehouse>Reno (Pick/Pack Saver)</Warehouse>
+ <Service>FESCO Ground</Service>
+ <Cost currency="USD">7.73</Cost>
+ <DeliveryEstimate>
+ <Minimum units="days">1</Minimum>
+ <Maximum units="days">5</Maximum>
+ </DeliveryEstimate>
+ </Quote>
+ </Quotes>
+ </Order>
+</RateResponse>
View
7 test/fixtures/xml/shipwire/no_rates_response.xml
@@ -0,0 +1,7 @@
+<RateResponse>
+ <Status>OK</Status>
+ <Order sequence="1">
+ <Quotes>
+ </Quotes>
+ </Order>
+</RateResponse>
View
36 test/fixtures/xml/shipwire/rates_response.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE RateResponse SYSTEM "http://www.shipwire.com/exec/download/RateResponse.dtd">
+<RateResponse>
+ <Status>OK</Status>
+ <Order sequence="1">
+ <Quotes>
+ <Quote method="GD">
+ <Warehouse>Reno (Pick/Pack Saver)</Warehouse>
+ <Service>UPS Ground</Service>
+ <Cost currency="USD">7.73</Cost>
+ <DeliveryEstimate>
+ <Minimum units="days">1</Minimum>
+ <Maximum units="days">5</Maximum>
+ </DeliveryEstimate>
+ </Quote>
+ <Quote method="2D">
+ <Warehouse>Reno (Pick/Pack Saver)</Warehouse>
+ <Service>UPS Second Day Air</Service>
+ <Cost currency="USD">13.64</Cost>
+ <DeliveryEstimate>
+ <Minimum units="days">2</Minimum>
+ <Maximum units="days">2</Maximum>
+ </DeliveryEstimate>
+ </Quote>
+ <Quote method="1D">
+ <Warehouse>Reno (Pick/Pack Saver)</Warehouse>
+ <Service>USPS Express Mail</Service>
+ <Cost currency="USD">25.25</Cost>
+ <DeliveryEstimate>
+ <Minimum units="days">1</Minimum>
+ <Maximum units="days">1</Maximum>
+ </DeliveryEstimate>
+ </Quote>
+ </Quotes>
+ </Order>
+</RateResponse>
View
77 test/remote/shipwire_test.rb
@@ -0,0 +1,77 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RemoteShipwireTest < Test::Unit::TestCase
+
+ def setup
+ @packages = TestFixtures.packages
+ @locations = TestFixtures.locations
+ @carrier = Shipwire.new(fixtures(:shipwire))
+ @item1 = { :sku => 'AF0001', :quantity => 2 }
+ @item2 = { :sku => 'AF0002', :quantity => 1 }
+ @items = [@item1, @item2]
+ end
+
+ def test_successful_domestic_rates_request_for_single_line_item
+ response = @carrier.find_rates(
+ @locations[:ottawa],
+ @locations[:beverly_hills],
+ @packages.values_at(:book, :wii),
+ :items => [@item1],
+ :order_id => '#1000'
+ )
+
+ assert response.success?
+ assert_equal 3, response.rates.size
+ assert_equal ['1D', '2D', 'GD'], response.rates.collect(&:service_code).sort
+ end
+
+ def test_successful_domestic_rates_request_for_multiple_line_items
+ response = @carrier.find_rates(
+ @locations[:ottawa],
+ @locations[:beverly_hills],
+ @packages.values_at(:book, :wii),
+ :items => @items,
+ :order_id => '#1000'
+ )
+
+ assert response.success?
+ assert_equal 3, response.rates.size
+ assert_equal ['1D', '2D', 'GD'], response.rates.collect(&:service_code).sort
+ end
+
+ def test_successful_international_rates_request_for_single_line_item
+ response = @carrier.find_rates(
+ @locations[:ottawa],
+ @locations[:london],
+ @packages.values_at(:book, :wii),
+ :items => [@item1],
+ :order_id => '#1000'
+ )
+
+ assert response.success?
+ assert_equal 1, response.rates.size
+ assert_equal ['INTL'], response.rates.collect(&:service_code)
+ end
+
+ def test_invalid_credentials
+ shipwire = Shipwire.new(
+ :login => 'your@email.com',
+ :password => 'password'
+ )
+
+ begin
+ assert_raises(ActiveMerchant::Shipping::ResponseError) do
+ shipwire.find_rates(
+ @locations[:ottawa],
+ @locations[:beverly_hills],
+ @packages.values_at(:book, :wii),
+ :items => @items,
+ :order_id => '#1000'
+ )
+ end
+ rescue ResponseError => e
+ assert_equal "Could not verify e-mail/password combination", response.message
+ end
+ end
+
+end
View
162 test/unit/carriers/shipwire_test.rb
@@ -1,60 +1,120 @@
require File.dirname(__FILE__) + '/../../test_helper'
class ShipwireTest < Test::Unit::TestCase
-
+
def setup
- @packages = TestFixtures.packages
- @locations = TestFixtures.locations
- @carrier = Shipwire.new(
- :login => 'login',
- :password => 'password'
- )
+ @packages = TestFixtures.packages
+ @locations = TestFixtures.locations
+ @carrier = Shipwire.new(:login => 'l', :password => 'p')
+ @items = [ { :sku => 'AF0001', :quantity => 1 }, { :sku => 'AF0002', :quantity => 2 } ]
+ end
+
+ def test_invalid_credentials
+ @carrier.expects(:ssl_post).returns(xml_fixture('shipwire/invalid_credentials_response'))
+
+ begin
+ @carrier.find_rates(
+ @locations[:ottawa],
+ @locations[:beverly_hills],
+ @packages.values_at(:book, :wii),
+ :order_id => '#1000',
+ :items => @items
+ )
+ rescue ResponseError => e
+ assert_equal "Could not verify e-mail/password combination", e.message
+ end
+ end
+
+ def test_response_with_no_rates_is_unsuccessful
+ @carrier.expects(:ssl_post).returns(xml_fixture('shipwire/no_rates_response'))
- @rate_response = xml_fixture('ups/shipment_from_tiger_direct')
+ assert_raises(ResponseError) do
+ response = @carrier.find_rates(
+ @locations[:ottawa],
+ @locations[:beverly_hills],
+ @packages.values_at(:book, :wii),
+ :order_id => '#1000',
+ :items => @items
+ )
+ end
end
- def test_truth
- assert true
+ def test_successfully_get_international_rates
+ @carrier.expects(:ssl_post).returns(xml_fixture('shipwire/international_rates_response'))
+
+ response = @carrier.find_rates(
+ @locations[:ottawa],
+ @locations[:london],
+ @packages.values_at(:book, :wii),
+ :order_id => '#1000',
+ :items => @items
+ )
+
+ assert response.success?
+
+ assert_equal 1, response.rates.size
+
+ assert international = response.rates.first
+ assert_equal "INTL", international.service_code
+ assert_equal "UPS", international.carrier
+ assert_equal "UPS Standard", international.service_name
+ assert_equal 2806, international.total_price
+ end
+
+ def test_successfully_get_domestic_rates
+ @carrier.expects(:ssl_post).returns(xml_fixture('shipwire/rates_response'))
+
+ response = @carrier.find_rates(
+ @locations[:ottawa],
+ @locations[:beverly_hills],
+ @packages.values_at(:book, :wii),
+ :order_id => '#1000',
+ :items => @items
+ )
+
+ assert response.success?
+
+ assert_equal 3, response.rates.size
+
+ assert ground = response.rates.find{|r| r.service_code == "GD" }
+ assert_equal "UPS", ground.carrier
+ assert_equal "UPS Ground", ground.service_name
+ assert_equal 773, ground.total_price
+
+ assert two_day = response.rates.find{|r| r.service_code == "2D" }
+ assert_equal "UPS", two_day.carrier
+ assert_equal "UPS Second Day Air", two_day.service_name
+ assert_equal 1364, two_day.total_price
+
+ assert one_day = response.rates.find{|r| r.service_code == "1D" }
+ assert_equal "USPS", one_day.carrier
+ assert_equal "USPS Express Mail", one_day.service_name
+ assert_equal 2525, one_day.total_price
end
- #def test_add_origin_and_destination_data_to_shipment_events_where_appropriate
- # Shipwire.any_instance.expects(:commit).returns(@tracking_response)
- # response = @carrier.find_tracking_info('1Z5FX0076803466397')
- # assert_equal '175 AMBASSADOR', response.shipment_events.first.location.address1
- # assert_equal 'K1N5X8', response.shipment_events.last.location.postal_code
- #end
- #
- #def test_response_parsing
- # mock_response = xml_fixture('ups/test_real_home_as_residential_destination_response')
- # Shipwire.any_instance.expects(:commit).returns(mock_response)
- # response = @carrier.find_rates( @locations[:beverly_hills],
- # @locations[:real_home_as_residential],
- # @packages.values_at(:chocolate_stuff))
- # assert_equal [ "Shipwire Ground",
- # "Shipwire Three-Day Select",
- # "Shipwire Second Day Air",
- # "Shipwire Next Day Air Saver",
- # "Shipwire Next Day Air Early A.M.",
- # "Shipwire Next Day Air"], response.rates.map(&:service_name)
- # assert_equal [992, 2191, 3007, 5509, 9401, 6124], response.rates.map(&:price)
- #end
- #
- #def test_xml_logging_to_file
- # mock_response = xml_fixture('ups/test_real_home_as_residential_destination_response')
- # Shipwire.any_instance.expects(:commit).times(2).returns(mock_response)
- # RateResponse.any_instance.expects(:log_xml).with({:name => 'test', :path => '/tmp/logs'}).times(1).returns(true)
- # response = @carrier.find_rates( @locations[:beverly_hills],
- # @locations[:real_home_as_residential],
- # @packages.values_at(:chocolate_stuff),
- # :log_xml => {:name => 'test', :path => '/tmp/logs'})
- # response = @carrier.find_rates( @locations[:beverly_hills],
- # @locations[:real_home_as_residential],
- # @packages.values_at(:chocolate_stuff))
- #end
- #
- #def test_maximum_weight
- # assert Package.new(150 * 16, [5,5,5], :units => :imperial).mass == @carrier.maximum_weight
- # assert Package.new((150 * 16) + 0.01, [5,5,5], :units => :imperial).mass > @carrier.maximum_weight
- # assert Package.new((150 * 16) - 0.01, [5,5,5], :units => :imperial).mass < @carrier.maximum_weight
- #end
-end
+ def test_gracefully_handle_new_carrier
+ @carrier.expects(:ssl_post).returns(xml_fixture('shipwire/new_carrier_rate_response'))
+
+ response = @carrier.find_rates(
+ @locations[:ottawa],
+ @locations[:beverly_hills],
+ @packages.values_at(:book, :wii),
+ :order_id => '#1000',
+ :items => @items
+ )
+ assert response.success?
+ assert_equal 1, response.rates.size
+ assert ground = response.rates.first
+ assert_equal "FESCO", ground.carrier
+ end
+
+ def test_find_rates_requires_items_option
+ assert_raises(ArgumentError) do
+ @carrier.find_rates(
+ @locations[:ottawa],
+ @locations[:beverly_hills],
+ @packages.values_at(:book, :wii)
+ )
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.