Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial import of project

  • Loading branch information...
commit 9b5ac5e4e9c52d27958f46e6c4d7c9a554457587 0 parents
Cody Fauser authored
Showing with 9,862 additions and 0 deletions.
  1. +3 −0  CHANGELOG
  2. +111 −0 Rakefile
  3. +1 −0  init.rb
  4. +51 −0 lib/active_fulfillment.rb
  5. +12 −0 lib/active_fulfillment/fulfillment/base.rb
  6. +30 −0 lib/active_fulfillment/fulfillment/response.rb
  7. +31 −0 lib/active_fulfillment/fulfillment/service.rb
  8. +3 −0  lib/active_fulfillment/fulfillment/services.rb
  9. +175 −0 lib/active_fulfillment/fulfillment/services/amazon.rb
  10. +199 −0 lib/active_fulfillment/fulfillment/services/shipwire.rb
  11. +198 −0 lib/active_fulfillment/fulfillment/services/webgistix.rb
  12. +297 −0 lib/active_fulfillment/lib/country.rb
  13. +4 −0 lib/active_fulfillment/lib/error.rb
  14. +22 −0 lib/active_fulfillment/lib/post_data.rb
  15. +80 −0 lib/active_fulfillment/lib/posts_data.rb
  16. +16 −0 lib/active_fulfillment/lib/requires_parameters.rb
  17. +76 −0 lib/active_fulfillment/lib/validateable.rb
  18. +7,815 −0 lib/certs/cacert.pem
  19. +11 −0 test/fixtures.yml
  20. +62 −0 test/remote/amazon_test.rb
  21. +124 −0 test/remote/shipwire_test.rb
  22. +59 −0 test/remote/webgistix_test.rb
  23. +60 −0 test/test_helper.rb
  24. +17 −0 test/unit/base_test.rb
  25. +193 −0 test/unit/services/amazon_test.rb
  26. +77 −0 test/unit/services/shipwire_test.rb
  27. +135 −0 test/unit/services/webgistix_test.rb
3  CHANGELOG
@@ -0,0 +1,3 @@
+# * Package for initial upload to Google Code
+# * Fix remote Webgistix test
+# * Add support for Fulfillment by Amazon Basic Fulfillment
111 Rakefile
@@ -0,0 +1,111 @@
+require 'rubygems'
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'rake/gempackagetask'
+require 'rake/contrib/rubyforgepublisher'
+
+
+PKG_VERSION = "0.0.1"
+PKG_NAME = "activeshipping"
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+
+PKG_FILES = FileList[
+ "lib/**/*", "examples/**/*", "[A-Z]*", "rakefile"
+].exclude(/\.svn$/)
+
+
+desc "Default Task"
+task :default => 'test:units'
+
+# Run the unit tests
+
+namespace :test do
+ Rake::TestTask.new(:units) do |t|
+ t.pattern = 'test/unit/**/*_test.rb'
+ t.ruby_opts << '-rubygems'
+ t.verbose = true
+ end
+
+ Rake::TestTask.new(:remote) do |t|
+ t.pattern = 'test/remote/*_test.rb'
+ t.ruby_opts << '-rubygems'
+ t.verbose = true
+ end
+end
+
+# Genereate the RDoc documentation
+Rake::RDocTask.new do |rdoc|
+ rdoc.rdoc_dir = 'doc'
+ rdoc.title = "ActiveFulfillment library"
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README', 'CHANGELOG')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+task :install => [:package] do
+ `gem install pkg/#{PKG_FILE_NAME}.gem`
+end
+
+task :lines do
+ lines = 0
+ codelines = 0
+ Dir.foreach("lib") { |file_name|
+ next unless file_name =~ /.*rb/
+
+ f = File.open("lib/" + file_name)
+
+ while line = f.gets
+ lines += 1
+ next if line =~ /^\s*$/
+ next if line =~ /^\s*#/
+ codelines += 1
+ end
+ }
+ puts "Lines #{lines}, LOC #{codelines}"
+end
+
+# Publish beta gem
+desc "Publish the beta gem"
+task :publish => [:rdoc, :package] do
+ Rake::SshFilePublisher.new("leetsoft.com", "dist/pkg", "pkg", "#{PKG_FILE_NAME}.zip").upload
+ Rake::SshFilePublisher.new("leetsoft.com", "dist/pkg", "pkg", "#{PKG_FILE_NAME}.tgz").upload
+ Rake::SshFilePublisher.new("leetsoft.com", "dist/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
+ `ssh tobi@leetsoft.com "mkdir -p dist/api/#{PKG_NAME}"`
+ Rake::SshDirPublisher.new("leetsoft.com", "dist/api/#{PKG_NAME}", "doc").upload
+ `ssh tobi@leetsoft.com './gemupdate'`
+end
+
+desc "Delete tar.gz / zip / rdoc"
+task :cleanup => [ :rm_packages, :clobber_rdoc ]
+
+task :install => [:package] do
+ `gem install pkg/#{PKG_FILE_NAME}.gem`
+end
+
+spec = Gem::Specification.new do |s|
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.summary = "Framework and tools for dealing with shipping, tracking and order fulfillment services."
+ s.has_rdoc = true
+
+ s.files = %w(README MIT-LICENSE CHANGELOG) + Dir['lib/**/*']
+
+ s.require_path = 'lib'
+ s.autorequire = 'active_fulfillment'
+ s.author = "Tobias Luetke"
+ s.email = "tobi@leetsoft.com"
+ s.homepage = "http://dist.leetsoft.com/"
+end
+
+Rake::GemPackageTask.new(spec) do |p|
+ p.gem_spec = spec
+ p.need_tar = true
+ p.need_zip = true
+end
+
+desc "Continuously watch unit tests"
+task :watch do
+ system("clear")
+ system("stakeout \"rake\" `find . -name '*.rb'`")
+end
1  init.rb
@@ -0,0 +1 @@
+require 'active_fulfillment'
51 lib/active_fulfillment.rb
@@ -0,0 +1,51 @@
+#--
+# Copyright (c) 2006 Jaded Pixel
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+$:.unshift File.dirname(__FILE__)
+
+begin
+ require 'active_support'
+rescue LoadError
+ require 'rubygems'
+ require 'active_support'
+end
+
+begin
+ require 'builder'
+rescue LoadError
+ require 'rubygems'
+ require_gem 'builder'
+end
+
+require 'cgi'
+require 'net/https'
+require 'active_fulfillment/lib/error'
+require 'active_fulfillment/lib/requires_parameters'
+require 'active_fulfillment/lib/posts_data'
+require 'active_fulfillment/lib/country'
+
+require 'active_fulfillment/fulfillment/base'
+require 'active_fulfillment/fulfillment/response'
+require 'active_fulfillment/fulfillment/service'
+require 'active_fulfillment/fulfillment/services'
+
12 lib/active_fulfillment/fulfillment/base.rb
@@ -0,0 +1,12 @@
+module ActiveMerchant
+ module Fulfillment
+ module Base
+ mattr_accessor :mode
+ self.mode = :production
+
+ def self.service(name)
+ ActiveMerchant::Fulfillment.const_get("#{name.to_s.downcase}_service".camelize)
+ end
+ end
+ end
+end
30 lib/active_fulfillment/fulfillment/response.rb
@@ -0,0 +1,30 @@
+module ActiveMerchant
+ module Fulfillment
+
+ class Error < StandardError
+ end
+
+ class Response
+ attr_reader :params
+ attr_reader :message
+ attr_reader :test
+
+ def success?
+ @success
+ end
+
+ def test?
+ @test
+ end
+
+ def initialize(success, message, params = {}, options = {})
+ @success, @message, @params = success, message, params.stringify_keys
+ @test = options[:test] || false
+ end
+
+ def method_missing(method, *args)
+ @params[method.to_s] || super
+ end
+ end
+ end
+end
31 lib/active_fulfillment/fulfillment/service.rb
@@ -0,0 +1,31 @@
+module ActiveMerchant
+ module Fulfillment
+ class Service
+
+ include RequiresParameters
+ include PostsData
+
+ def initialize(options = {})
+ check_test_mode(options)
+
+ @options = {}
+ @options.update(options)
+ end
+
+ def test_mode?
+ false
+ end
+
+ def test?
+ @options[:test] || Base.mode == :test
+ end
+
+ private
+ def check_test_mode(options)
+ if options[:test] and not test_mode?
+ raise ArgumentError, 'Test mode is not supported by this gateway'
+ end
+ end
+ end
+ end
+end
3  lib/active_fulfillment/fulfillment/services.rb
@@ -0,0 +1,3 @@
+require 'active_fulfillment/fulfillment/services/shipwire'
+require 'active_fulfillment/fulfillment/services/webgistix'
+require 'active_fulfillment/fulfillment/services/amazon'
175 lib/active_fulfillment/fulfillment/services/amazon.rb
@@ -0,0 +1,175 @@
+module ActiveMerchant
+ module Fulfillment
+ class AmazonService < Service
+ URL = 'https://fba.amazonaws.com/FulfillmentService'
+
+ SUCCESS, FAILURE, ERROR = 'Accepted', 'Failure', 'Error'
+ MESSAGES = {
+ :success => 'Successfully submitted the order',
+ :failure => 'Failed to submit the order',
+ :error => 'An error occurred while submitting the order'
+ }
+
+ XMLNS = "http://fulfillment.amazonaws.com/doc/FulfillmentService/2006-12-12"
+
+ # The first is the label, and the last is the code
+ # Standard: 3-5 business days
+ # Expedited: 2 business days
+ # Priority: 1 business day
+ def self.shipping_methods
+ ActiveSupport::OrderedHash.new(
+ [ [ 'Standard Shipping', 'Standard' ],
+ [ 'Expedited Shipping', 'Expedited' ],
+ [ 'Priority Shipping', 'Priority' ] ]
+ )
+ end
+
+ def initialize(options = {})
+ requires!(options, :login, :password)
+ super
+
+ @url = URL
+ end
+
+ def fulfill(order_id, shipping_address, line_items, options = {})
+ requires!(options, :order_date, :comment, :shipping_method)
+ commit build_fulfillment_request(order_id, shipping_address, line_items, options)
+ end
+
+ def fetch_current_orders
+ commit build_get_current_fulfillment_orders_request
+ end
+
+ def test_mode?
+ false
+ end
+
+ private
+
+ def build_get_current_fulfillment_orders_request
+ xml = Builder::XmlMarkup.new
+
+ xml.tag! 'GetCurrentFulfillmentOrders', { 'xmlns' => XMLNS } do
+ xml.tag! 'Request' do
+ add_credentials(xml)
+
+ xml.tag! "MaxResultsRequested", 5
+ xml.tag! "QueryStartDateTime", Time.now.yesterday.strftime("%Y-%m-%d %H:%M:%S")
+ end
+ end
+ xml.target!
+ end
+
+ def build_fulfillment_request(order_id, shipping_address, line_items, options)
+ xml = Builder::XmlMarkup.new
+
+ xml.tag! 'CreateFulfillmentOrder', { 'xmlns' => XMLNS } do
+ xml.tag! 'Request' do
+ add_credentials(xml)
+
+ xml.tag! "MerchantFulfillmentOrderId", order_id
+ xml.tag! "DisplayableOrderId", order_id
+ xml.tag! "DisplayableOrderDate", options[:order_date].strftime("%Y-%m-%dT%H:%M:%SZ")
+ xml.tag! "DisplayableOrderComment", options[:comment]
+ xml.tag! "DeliverySLA", options[:shipping_method]
+
+ add_address(xml, shipping_address)
+ add_items(xml, line_items)
+ end
+ end
+ xml.target!
+ end
+
+ def add_credentials(xml)
+ xml.tag! 'Credentials' do
+ xml.tag! 'Email', @options[:login]
+ xml.tag! 'Password', @options[:password]
+ end
+ end
+
+ def add_items(xml, line_items)
+ Array(line_items).each_with_index do |item, index|
+ xml.tag! 'Items' do
+ xml.tag! 'MerchantSKU', item[:sku]
+ xml.tag! "MerchantFulfillmentOrderItemId", index
+ xml.tag! "Quantity", item[:quantity]
+ xml.tag! "GiftMessage", item[:gift_message] unless item[:gift_message].blank?
+ xml.tag! "DisplayableComment", item[:comment] unless item[:comment].blank?
+ end
+ end
+ end
+
+ def add_address(xml, address)
+ xml.tag! 'DestinationAddress' do
+ xml.tag! 'Name', address[:name]
+ xml.tag! 'Line1', address[:address1]
+ xml.tag! 'Line2', address[:address2] unless address[:address2].blank?
+ xml.tag! 'Line3', address[:address3] unless address[:address3].blank?
+ xml.tag! 'City', address[:city]
+ xml.tag! 'StateOrRegion', address[:state]
+ xml.tag! 'CountryCode', address[:country]
+ xml.tag! 'PostalCode', address[:zip]
+ xml.tag! 'PhoneNumber', address[:phone] unless address[:phone].blank?
+ end
+ end
+
+ def build_request(body)
+ xml = Builder::XmlMarkup.new
+
+ xml.instruct!
+ xml.tag! 'env:Envelope', { 'xmlns:env' => 'http://schemas.xmlsoap.org/soap/envelope/' } do
+ xml.tag! 'env:Body' do
+ xml << body
+ end
+ end
+ xml.target!
+ end
+
+ def commit(body)
+ data = ssl_post(@url, build_request(body), 'Content-Type' => 'application/soap+xml; charset=utf-8')
+ @response = parse(data)
+
+ Response.new(success?(@response), message_from(@response), @response,
+ :test => false
+ )
+ end
+
+ def success?(response)
+ response[:response_status] == SUCCESS
+ end
+
+ def message_from(response)
+ success?(response) ? MESSAGES[:success] : response[:response_comment] || response[:faultstring]
+ end
+
+ def parse(xml)
+ response = {}
+
+ begin
+ document = REXML::Document.new(xml)
+
+ node = REXML::XPath.first(document, '//ns1:Response') || REXML::XPath.first(document, '//env:Fault')
+ if node
+ parse_elements(response, node)
+ else
+ response[:response_status] = FAILURE
+ response[:response_comment] = MESSAGES[:failure]
+ end
+ rescue REXML::ParseException
+ response[:response_status] = ERROR
+ response[:response_comment] = MESSAGES[:error]
+ end
+
+ response
+ end
+
+ def parse_elements(response, node)
+ node.elements.each do |e|
+ response[e.name.underscore.to_sym] = e.text.to_s.gsub("\n", " ").strip
+ end
+ end
+ end
+ end
+end
+
+
199 lib/active_fulfillment/fulfillment/services/shipwire.rb
@@ -0,0 +1,199 @@
+require 'cgi'
+
+module ActiveMerchant
+ module Fulfillment
+ class ShipwireService < Service
+ SERVICE_URLS = { :fulfillment => 'https://www.shipwire.com/exec/FulfillmentServices.php',
+ :inventory => 'https://www.shipwire.com/exec/InventoryServices.php',
+ :tracking => 'https://www.shipwire.com/exec/TrackingServices.php'
+ }
+
+ SCHEMA_URLS = { :fulfillment => 'http://www.shipwire.com/exec/download/OrderList.dtd',
+ :inventory => 'http://www.shipwire.com/exec/download/InventoryUpdate.dtd',
+ :tracking => 'http://www.shipwire.com/exec/download/TrackingUpdate.dtd'
+ }
+
+ POST_VARS = { :fulfillment => 'OrderListXML',
+ :inventory => 'InventoryUpdateXML',
+ :tracking => 'TrackingUpdateXML'
+ }
+
+ WAREHOUSES = { '01' => '01 - Shipwire Chicago',
+ '02' => '02 - Shipwire Los Angeles'
+ }
+
+ # The first is the label, and the last is the code
+ def self.shipping_methods
+ ActiveSupport::OrderedHash.new(
+ [ ['1 Day Service', '1D'],
+ ['2 Day Service', '2D'],
+ ['Ground Service', 'GD'],
+ ['Freight Service', 'FT'] ]
+ )
+ end
+
+ # Pass in the login and password for the shipwire account.
+ # Optionally pass in the :test => true to force test mode
+ def initialize(options = {})
+ requires!(options, :login, :password)
+
+ super
+ end
+
+ def fulfill(order_id, shipping_address, line_items, options = {})
+ commit :fulfillment, build_fulfillment_request(order_id, shipping_address, line_items, options)
+ end
+
+ def fetch_stock_levels(options = {})
+ commit :inventory, build_inventory_request(options)
+ end
+
+ def test_mode?
+ true
+ end
+
+ private
+ def build_fulfillment_request(order_id, shipping_address, line_items, options)
+ xml = Builder::XmlMarkup.new :indent => 2
+ xml.instruct!
+ xml.declare! :DOCTYPE, :OrderList, :SYSTEM, SCHEMA_URLS[:fulfillment]
+ xml.tag! 'OrderList' do
+ add_credentials(xml)
+ xml.tag! 'Referer', 'SHOPIFY'
+ add_order(xml, order_id, shipping_address, line_items, options)
+ end
+ xml.target!
+ end
+
+ def build_inventory_request(options)
+ xml = Builder::XmlMarkup.new :indent => 2
+ xml.instruct!
+ xml.declare! :DOCTYPE, :InventoryStatus, :SYSTEM, SCHEMA_URLS[:inventory]
+ xml.tag! 'InventoryUpdate' do
+ add_credentials(xml)
+ xml.tag! 'Warehouse', WAREHOUSES[options[:warehouse]]
+ xml.tag! 'ProductCode', options[:sku]
+ end
+ end
+
+ def add_credentials(xml)
+ xml.tag! 'EmailAddress', @options[:login]
+ xml.tag! 'Password', @options[:password]
+ xml.tag! 'Server', test? ? 'Test' : 'Production'
+ end
+
+ def add_order(xml, order_id, shipping_address, line_items, options)
+ xml.tag! 'Order', :id => order_id do
+ xml.tag! 'Warehouse', options[:warehouse] || '00'
+
+ add_address(xml, shipping_address)
+ xml.tag! 'Shipping', options[:shipping_method] unless options[:shipping_method].blank?
+
+ Array(line_items).each_with_index do |line_item, index|
+ add_item(xml, line_item, index)
+ end
+ end
+ end
+
+ def add_address(xml, address)
+ xml.tag! 'AddressInfo', :type => 'Ship' do
+ xml.tag! 'Name' do
+ xml.tag! 'Full', address[:name]
+ end
+ xml.tag! 'Address1', address[:address1] unless address[:address1].blank?
+ xml.tag! 'Address2', address[:address2] unless address[:address2].blank?
+ xml.tag! 'City', address[:city] unless address[:city].blank?
+ xml.tag! 'State', address[:state] unless address[:state].blank?
+
+ unless address[:country].blank?
+ country = Country.find(address[:country])
+
+ # Special handling for the United Kingdom, as they use the top level domain for the code
+ country_code = country.code(:alpha2).to_s == 'GB' ? 'UK' : country.code(:alpha2)
+
+ xml.tag! 'Country', "#{country_code} #{country.name}"
+ end
+
+ xml.tag! 'Zip', address[:zip] unless address[:zip].blank?
+ xml.tag! 'Phone', address[:phone] unless address[:phone].blank?
+ xml.tag! 'Email', address[:email] unless address[:email].blank?
+ end
+ end
+
+ def add_item(xml, item, index)
+ xml.tag! 'Item', :num => index do
+ # Code is limited to 12 character
+ xml.tag! 'Code', item[:sku] unless item[:sku].blank?
+ xml.tag! 'Quantity', item[:quantity] unless item[:quantity].blank?
+ xml.tag! 'Description', item[:description] unless item[:description].blank?
+ xml.tag! 'Length', item[:length] unless item[:length].blank?
+ xml.tag! 'Width', item[:width] unless item[:width].blank?
+ xml.tag! 'Height', item[:height] unless item[:height].blank?
+ xml.tag! 'Weight', item[:weight] unless item[:weight].blank?
+ xml.tag! 'DeclaredValue', item[:declared_value] unless item[:declared_value].blank?
+ end
+ end
+
+ def commit(action, request)
+ data = ssl_post(SERVICE_URLS[action],
+ "#{POST_VARS[action]}=#{CGI.escape(request)}",
+ 'Content-Type' => 'application/x-www-form-urlencoded'
+ )
+
+ case action
+ when :fulfillment
+
+ @response = parse_fulfillment_response(data)
+ success = @response[:status] == '0'
+ message = success ? "Successfully submitted the order" : message_from(@response[:error_message])
+
+
+ when :inventory
+ @response = parse_inventory_response(data)
+ if test?
+ success = @response[:status] == 'Test'
+ else
+ success = @response[:status] == '0'
+ end
+ message = success ? "Successfully received the stock levels" : message_from(@response[:error_message])
+ end
+
+ Response.new(success, message, @response,
+ :test => test?
+ )
+ end
+
+ def parse_fulfillment_response(xml)
+ response = {}
+
+ document = REXML::Document.new(xml)
+ document.root.elements.each do |node|
+ response[node.name.underscore.to_sym] = node.text
+ end
+ response
+ end
+
+ def parse_inventory_response(xml)
+ response = {}
+ response[:stock_levels] = {}
+
+ document = REXML::Document.new(xml)
+ document.root.elements.each do |node|
+ if node.name == 'Product'
+ response[:stock_levels][node.attributes['code']] = node.attributes['quantity'].to_i
+ else
+ response[node.name.underscore.to_sym] = node.text
+ end
+ end
+ response
+ end
+
+ def message_from(string)
+ return if string.blank?
+ string.gsub("\n", '').squeeze(" ")
+ end
+ end
+ end
+end
+
+
198 lib/active_fulfillment/fulfillment/services/webgistix.rb
@@ -0,0 +1,198 @@
+module ActiveMerchant
+ module Fulfillment
+ class WebgistixService < Service
+ TEST_URL = 'https://www.webgistix.com/XML/shippingTest.asp'
+ LIVE_URL = 'https://www.webgistix.com/XML/API.asp'
+
+ SUCCESS, FAILURE = 'True', 'False'
+ SUCCESS_MESSAGE = 'Successfully submitted the order'
+ FAILURE_MESSAGE = 'Failed to submit the order'
+
+ # The first is the label, and the last is the code
+ def self.shipping_methods
+ ActiveSupport::OrderedHash.new(
+ [ ["UPS Ground Shipping", "Ground"],
+ ["UPS Standard Shipping (Canada Only)", "Standard"],
+ ["UPS 3-Business Day", "3-Day Select"],
+ ["UPS 2-Business Day", "2nd Day Air"],
+ ["UPS 2-Business Day AM", "2nd Day Air AM"],
+ ["UPS Next Day", "Next Day Air"],
+ ["UPS Next Day Saver", "Next Day Air Saver"],
+ ["UPS Next Day Early AM", "Next Day Air Early AM"],
+ ["UPS Worldwide Express (Next Day)", "Worldwide Express"],
+ ["UPS Worldwide Expedited (2nd Day)", "Worldwide Expedited"],
+ ["UPS Worldwide Express Saver", "Worldwide Express Saver"],
+ ["FedEx Priority Overnight", "FedEx Priority Overnight"],
+ ["FedEx Standard Overnight", "FedEx Standard Overnight"],
+ ["FedEx First Overnight", "FedEx First Overnight"],
+ ["FedEx 2nd Day", "FedEx 2nd Day"],
+ ["FedEx Express Saver", "FedEx Express Saver"],
+ ["FedEx International Priority", "FedEx International Priority"],
+ ["FedEx International Economy", "FedEx International Economy"],
+ ["FedEx International First", "FedEx International First"],
+ ["FedEx Ground", "FedEx Ground"],
+ ["USPS Priority Mail & Global Priority Mail", "Priority"],
+ ["USPS First Class Mail", "First Class"],
+ ["USPS Express Mail & Global Express Mail", "Express"],
+ ["USPS Parcel Post", "Parcel"],
+ ["USPS Air Letter Post", "Air Letter Post"],
+ ["USPS Media Mail", "Media Mail"],
+ ["USPS Economy Parcel Post", "Economy Parcel"],
+ ["USPS Economy Air Letter Post", "Economy Letter"],
+ ["DHL Express", "DHL Express"],
+ ["DHL Next Afternoon", "DHL Next Afternoon"],
+ ["DHL Second Day Service", "DHL Second Day Service"],
+ ["DHL Ground", "DHL Ground"],
+ ["DHL International Express", "DHL International Express"] ]
+ )
+ end
+
+ # Pass in the login and password for the shipwire account.
+ # Optionally pass in the :test => true to force test mode
+ def initialize(options = {})
+ requires!(options, :login, :password)
+ super
+ @url = test? ? TEST_URL : LIVE_URL
+ end
+
+ def fulfill(order_id, shipping_address, line_items, options = {})
+ requires!(options, :shipping_method)
+ commit build_fulfillment_request(order_id, shipping_address, line_items, options)
+ end
+
+ def test_mode?
+ true
+ end
+
+ private
+ #<?xml version="1.0"?>
+ # <OrderXML>
+ # <Password>Webgistix</Password>
+ # <CustomerID>3</CustomerID>
+ # <Order>
+ # <ReferenceNumber></ReferenceNumber>
+ # <Company>Test Company</Company>
+ # <Name>Joe Smith</Name>
+ # <Address1>123 Main St.</Address1>
+ # <Address2></Address2>
+ # <Address3></Address3>
+ # <City>Olean</City>
+ # <State>NY</State>
+ # <ZipCode>14760</ZipCode>
+ # <Country>United States</Country>
+ # <Email>info@webgistix.com</Email>
+ # <Phone>1-123-456-7890</Phone>
+ # <ShippingInstructions>Ground</ShippingInstructions>
+ # <OrderComments>Test Order</OrderComments>
+ # <Approve>0</Approve>
+ # <Item>
+ # <ItemID>testitem</ItemID>
+ # <ItemQty>2</ItemQty>
+ # </Item>
+ # </Order>
+ # </OrderXML>
+ def build_fulfillment_request(order_id, shipping_address, line_items, options)
+ xml = Builder::XmlMarkup.new :indent => 2
+ xml.instruct!
+ xml.tag! 'OrderXML' do
+ add_credentials(xml)
+ add_order(xml, order_id, shipping_address, line_items, options)
+ end
+ xml.target!
+ end
+
+ def add_credentials(xml)
+ xml.tag! 'CustomerID', @options[:login]
+ xml.tag! 'Password', @options[:password]
+ end
+
+ def add_order(xml, order_id, shipping_address, line_items, options)
+ xml.tag! 'Order' do
+ xml.tag! 'ReferenceNumber', order_id
+ xml.tag! 'ShippingInstructions', options[:shipping_method]
+ xml.tag! 'Approve', 1
+ xml.tag! 'OrderComments', options[:comment] unless options[:comment].blank?
+
+ add_address(xml, shipping_address)
+
+ Array(line_items).each_with_index do |line_item, index|
+ add_item(xml, line_item, index)
+ end
+ end
+ end
+
+ def add_address(xml, address)
+ xml.tag! 'Name', address[:name]
+ xml.tag! 'Address1', address[:address1]
+ xml.tag! 'Address2', address[:address2] unless address[:address2].blank?
+ xml.tag! 'Address3', address[:address3] unless address[:address3].blank?
+ xml.tag! 'City', address[:city]
+ xml.tag! 'State', address[:state]
+ xml.tag! 'ZipCode', address[:zip]
+ xml.tag! 'Company', address[:company]
+
+ unless address[:country].blank?
+ country = Country.find(address[:country])
+ xml.tag! 'Country', country.name
+ end
+
+ xml.tag! 'Phone', address[:phone]
+ xml.tag! 'Email', address[:email] unless address[:email].blank?
+ end
+
+ def add_item(xml, item, index)
+ xml.tag! 'Item' do
+ xml.tag! 'ItemID', item[:sku] unless item[:sku].blank?
+ xml.tag! 'ItemQty', item[:quantity] unless item[:quantity].blank?
+ end
+ end
+
+ def commit(request)
+ @response = parse(ssl_post(@url, request,
+ 'EndPointURL' => @url,
+ 'Content-Type' => 'text/xml; charset="utf-8"')
+ )
+
+ Response.new(success?(@response), message_from(@response), @response,
+ :test => test?
+ )
+ end
+
+ def success?(response)
+ response[:success] == SUCCESS
+ end
+
+ def message_from(response)
+ success?(response) ? SUCCESS_MESSAGE : FAILURE_MESSAGE
+ end
+
+ def parse(xml)
+ response = {}
+
+ begin
+ document = REXML::Document.new("<response>#{xml}</response>")
+ rescue REXML::ParseException
+ response[:success] = FAILURE
+ return response
+ end
+ # Fetch the errors
+ document.root.elements.to_a("Error").each_with_index do |e, i|
+ response["error_#{i}".to_sym] = e.text
+ end
+
+ # Check if completed
+ if completed = REXML::XPath.first(document, '//Completed')
+ completed.elements.each do |e|
+ response[e.name.underscore.to_sym] = e.text
+ end
+ else
+ response[:success] = FAILURE
+ end
+
+ response
+ end
+ end
+ end
+end
+
+
297 lib/active_fulfillment/lib/country.rb
@@ -0,0 +1,297 @@
+module ActiveMerchant #:nodoc:
+ class InvalidCountryCodeError < StandardError
+ end
+
+ class CountryCodeFormatError < StandardError
+ end
+
+ class CountryCode
+ attr_reader :value, :format
+ def initialize(value)
+ @value = value.to_s.upcase
+ detect_format
+ end
+
+ def to_s
+ value
+ end
+
+ private
+
+ def detect_format
+ case @value
+ when /^[[:alpha:]]{2}$/
+ @format = :alpha2
+ when /^[[:alpha:]]{3}$/
+ @format = :alpha3
+ when /^[[:digit:]]{3}$/
+ @format = :numeric
+ else
+ raise CountryCodeFormatError, "The country code is not formatted correctly #{@value}"
+ end
+ end
+ end
+
+ class Country
+ include RequiresParameters
+ attr_reader :name
+
+ def initialize(options = {})
+ requires!(options, :name, :alpha2, :alpha3, :numeric)
+ @name = options.delete(:name)
+ @codes = options.collect{|k,v| CountryCode.new(v)}
+ end
+
+ def code(format)
+ @codes.select{|c| c.format == format}
+ end
+
+ def to_s
+ @name
+ end
+
+ COUNTRIES = [
+ { :alpha2 => 'AF', :name => 'Afghanistan', :alpha3 => 'AFG', :numeric => '004' },
+ { :alpha2 => 'AL', :name => 'Albania', :alpha3 => 'ALB', :numeric => '008' },
+ { :alpha2 => 'DZ', :name => 'Algeria', :alpha3 => 'DZA', :numeric => '012' },
+ { :alpha2 => 'AS', :name => 'American Samoa', :alpha3 => 'ASM', :numeric => '016' },
+ { :alpha2 => 'AD', :name => 'Andorra', :alpha3 => 'AND', :numeric => '020' },
+ { :alpha2 => 'AO', :name => 'Angola', :alpha3 => 'AGO', :numeric => '024' },
+ { :alpha2 => 'AI', :name => 'Anguilla', :alpha3 => 'AIA', :numeric => '660' },
+ { :alpha2 => 'AG', :name => 'Antigua and Barbuda', :alpha3 => 'ATG', :numeric => '028' },
+ { :alpha2 => 'AR', :name => 'Argentina', :alpha3 => 'ARG', :numeric => '032' },
+ { :alpha2 => 'AM', :name => 'Armenia', :alpha3 => 'ARM', :numeric => '051' },
+ { :alpha2 => 'AW', :name => 'Aruba', :alpha3 => 'ABW', :numeric => '533' },
+ { :alpha2 => 'AU', :name => 'Australia', :alpha3 => 'AUS', :numeric => '036' },
+ { :alpha2 => 'AT', :name => 'Austria', :alpha3 => 'AUT', :numeric => '040' },
+ { :alpha2 => 'AZ', :name => 'Azerbaijan', :alpha3 => 'AZE', :numeric => '031' },
+ { :alpha2 => 'BS', :name => 'Bahamas', :alpha3 => 'BHS', :numeric => '044' },
+ { :alpha2 => 'BH', :name => 'Bahrain', :alpha3 => 'BHR', :numeric => '048' },
+ { :alpha2 => 'BD', :name => 'Bangladesh', :alpha3 => 'BGD', :numeric => '050' },
+ { :alpha2 => 'BB', :name => 'Barbados', :alpha3 => 'BRB', :numeric => '052' },
+ { :alpha2 => 'BY', :name => 'Belarus', :alpha3 => 'BLR', :numeric => '112' },
+ { :alpha2 => 'BE', :name => 'Belgium', :alpha3 => 'BEL', :numeric => '056' },
+ { :alpha2 => 'BZ', :name => 'Belize', :alpha3 => 'BLZ', :numeric => '084' },
+ { :alpha2 => 'BJ', :name => 'Benin', :alpha3 => 'BEN', :numeric => '204' },
+ { :alpha2 => 'BM', :name => 'Bermuda', :alpha3 => 'BMU', :numeric => '060' },
+ { :alpha2 => 'BT', :name => 'Bhutan', :alpha3 => 'BTN', :numeric => '064' },
+ { :alpha2 => 'BO', :name => 'Bolivia', :alpha3 => 'BOL', :numeric => '068' },
+ { :alpha2 => 'BA', :name => 'Bosnia and Herzegovina', :alpha3 => 'BIH', :numeric => '070' },
+ { :alpha2 => 'BW', :name => 'Botswana', :alpha3 => 'BWA', :numeric => '072' },
+ { :alpha2 => 'BR', :name => 'Brazil', :alpha3 => 'BRA', :numeric => '076' },
+ { :alpha2 => 'BN', :name => 'Brunei Darussalam', :alpha3 => 'BRN', :numeric => '096' },
+ { :alpha2 => 'BG', :name => 'Bulgaria', :alpha3 => 'BGR', :numeric => '100' },
+ { :alpha2 => 'BF', :name => 'Burkina Faso', :alpha3 => 'BFA', :numeric => '854' },
+ { :alpha2 => 'BI', :name => 'Burundi', :alpha3 => 'BDI', :numeric => '108' },
+ { :alpha2 => 'KH', :name => 'Cambodia', :alpha3 => 'KHM', :numeric => '116' },
+ { :alpha2 => 'CM', :name => 'Cameroon', :alpha3 => 'CMR', :numeric => '120' },
+ { :alpha2 => 'CA', :name => 'Canada', :alpha3 => 'CAN', :numeric => '124' },
+ { :alpha2 => 'CV', :name => 'Cape Verde', :alpha3 => 'CPV', :numeric => '132' },
+ { :alpha2 => 'KY', :name => 'Cayman Islands', :alpha3 => 'CYM', :numeric => '136' },
+ { :alpha2 => 'CF', :name => 'Central African Republic', :alpha3 => 'CAF', :numeric => '140' },
+ { :alpha2 => 'TD', :name => 'Chad', :alpha3 => 'TCD', :numeric => '148' },
+ { :alpha2 => 'CL', :name => 'Chile', :alpha3 => 'CHL', :numeric => '152' },
+ { :alpha2 => 'CN', :name => 'China', :alpha3 => 'CHN', :numeric => '156' },
+ { :alpha2 => 'CO', :name => 'Colombia', :alpha3 => 'COL', :numeric => '170' },
+ { :alpha2 => 'KM', :name => 'Comoros', :alpha3 => 'COM', :numeric => '174' },
+ { :alpha2 => 'CG', :name => 'Congo', :alpha3 => 'COG', :numeric => '178' },
+ { :alpha2 => 'CD', :name => 'Congo, the Democratic Republic of the', :alpha3 => 'COD', :numeric => '180' },
+ { :alpha2 => 'CK', :name => 'Cook Islands', :alpha3 => 'COK', :numeric => '184' },
+ { :alpha2 => 'CR', :name => 'Costa Rica', :alpha3 => 'CRI', :numeric => '188' },
+ { :alpha2 => 'CI', :name => 'Cote D\'Ivoire', :alpha3 => 'CIV', :numeric => '384' },
+ { :alpha2 => 'HR', :name => 'Croatia', :alpha3 => 'HRV', :numeric => '191' },
+ { :alpha2 => 'CU', :name => 'Cuba', :alpha3 => 'CUB', :numeric => '192' },
+ { :alpha2 => 'CY', :name => 'Cyprus', :alpha3 => 'CYP', :numeric => '196' },
+ { :alpha2 => 'CZ', :name => 'Czech Republic', :alpha3 => 'CZE', :numeric => '203' },
+ { :alpha2 => 'DK', :name => 'Denmark', :alpha3 => 'DNK', :numeric => '208' },
+ { :alpha2 => 'DJ', :name => 'Djibouti', :alpha3 => 'DJI', :numeric => '262' },
+ { :alpha2 => 'DM', :name => 'Dominica', :alpha3 => 'DMA', :numeric => '212' },
+ { :alpha2 => 'DO', :name => 'Dominican Republic', :alpha3 => 'DOM', :numeric => '214' },
+ { :alpha2 => 'EC', :name => 'Ecuador', :alpha3 => 'ECU', :numeric => '218' },
+ { :alpha2 => 'EG', :name => 'Egypt', :alpha3 => 'EGY', :numeric => '818' },
+ { :alpha2 => 'SV', :name => 'El Salvador', :alpha3 => 'SLV', :numeric => '222' },
+ { :alpha2 => 'GQ', :name => 'Equatorial Guinea', :alpha3 => 'GNQ', :numeric => '226' },
+ { :alpha2 => 'ER', :name => 'Eritrea', :alpha3 => 'ERI', :numeric => '232' },
+ { :alpha2 => 'EE', :name => 'Estonia', :alpha3 => 'EST', :numeric => '233' },
+ { :alpha2 => 'ET', :name => 'Ethiopia', :alpha3 => 'ETH', :numeric => '231' },
+ { :alpha2 => 'FK', :name => 'Falkland Islands (Malvinas)', :alpha3 => 'FLK', :numeric => '238' },
+ { :alpha2 => 'FO', :name => 'Faroe Islands', :alpha3 => 'FRO', :numeric => '234' },
+ { :alpha2 => 'FJ', :name => 'Fiji', :alpha3 => 'FJI', :numeric => '242' },
+ { :alpha2 => 'FI', :name => 'Finland', :alpha3 => 'FIN', :numeric => '246' },
+ { :alpha2 => 'FR', :name => 'France', :alpha3 => 'FRA', :numeric => '250' },
+ { :alpha2 => 'GF', :name => 'French Guiana', :alpha3 => 'GUF', :numeric => '254' },
+ { :alpha2 => 'PF', :name => 'French Polynesia', :alpha3 => 'PYF', :numeric => '258' },
+ { :alpha2 => 'GA', :name => 'Gabon', :alpha3 => 'GAB', :numeric => '266' },
+ { :alpha2 => 'GM', :name => 'Gambia', :alpha3 => 'GMB', :numeric => '270' },
+ { :alpha2 => 'GE', :name => 'Georgia', :alpha3 => 'GEO', :numeric => '268' },
+ { :alpha2 => 'DE', :name => 'Germany', :alpha3 => 'DEU', :numeric => '276' },
+ { :alpha2 => 'GH', :name => 'Ghana', :alpha3 => 'GHA', :numeric => '288' },
+ { :alpha2 => 'GI', :name => 'Gibraltar', :alpha3 => 'GIB', :numeric => '292' },
+ { :alpha2 => 'GR', :name => 'Greece', :alpha3 => 'GRC', :numeric => '300' },
+ { :alpha2 => 'GL', :name => 'Greenland', :alpha3 => 'GRL', :numeric => '304' },
+ { :alpha2 => 'GD', :name => 'Grenada', :alpha3 => 'GRD', :numeric => '308' },
+ { :alpha2 => 'GP', :name => 'Guadeloupe', :alpha3 => 'GLP', :numeric => '312' },
+ { :alpha2 => 'GU', :name => 'Guam', :alpha3 => 'GUM', :numeric => '316' },
+ { :alpha2 => 'GT', :name => 'Guatemala', :alpha3 => 'GTM', :numeric => '320' },
+ { :alpha2 => 'GN', :name => 'Guinea', :alpha3 => 'GIN', :numeric => '324' },
+ { :alpha2 => 'GW', :name => 'Guinea-Bissau', :alpha3 => 'GNB', :numeric => '624' },
+ { :alpha2 => 'GY', :name => 'Guyana', :alpha3 => 'GUY', :numeric => '328' },
+ { :alpha2 => 'HT', :name => 'Haiti', :alpha3 => 'HTI', :numeric => '332' },
+ { :alpha2 => 'VA', :name => 'Holy See (Vatican City State)', :alpha3 => 'VAT', :numeric => '336' },
+ { :alpha2 => 'HN', :name => 'Honduras', :alpha3 => 'HND', :numeric => '340' },
+ { :alpha2 => 'HK', :name => 'Hong Kong', :alpha3 => 'HKG', :numeric => '344' },
+ { :alpha2 => 'HU', :name => 'Hungary', :alpha3 => 'HUN', :numeric => '348' },
+ { :alpha2 => 'IS', :name => 'Iceland', :alpha3 => 'ISL', :numeric => '352' },
+ { :alpha2 => 'IN', :name => 'India', :alpha3 => 'IND', :numeric => '356' },
+ { :alpha2 => 'ID', :name => 'Indonesia', :alpha3 => 'IDN', :numeric => '360' },
+ { :alpha2 => 'IR', :name => 'Iran, Islamic Republic of', :alpha3 => 'IRN', :numeric => '364' },
+ { :alpha2 => 'IQ', :name => 'Iraq', :alpha3 => 'IRQ', :numeric => '368' },
+ { :alpha2 => 'IE', :name => 'Ireland', :alpha3 => 'IRL', :numeric => '372' },
+ { :alpha2 => 'IL', :name => 'Israel', :alpha3 => 'ISR', :numeric => '376' },
+ { :alpha2 => 'IT', :name => 'Italy', :alpha3 => 'ITA', :numeric => '380' },
+ { :alpha2 => 'JM', :name => 'Jamaica', :alpha3 => 'JAM', :numeric => '388' },
+ { :alpha2 => 'JP', :name => 'Japan', :alpha3 => 'JPN', :numeric => '392' },
+ { :alpha2 => 'JO', :name => 'Jordan', :alpha3 => 'JOR', :numeric => '400' },
+ { :alpha2 => 'KZ', :name => 'Kazakhstan', :alpha3 => 'KAZ', :numeric => '398' },
+ { :alpha2 => 'KE', :name => 'Kenya', :alpha3 => 'KEN', :numeric => '404' },
+ { :alpha2 => 'KI', :name => 'Kiribati', :alpha3 => 'KIR', :numeric => '296' },
+ { :alpha2 => 'KP', :name => 'Korea, Democratic People\'s Republic of', :alpha3 => 'PRK', :numeric => '408' },
+ { :alpha2 => 'KR', :name => 'Korea, Republic of', :alpha3 => 'KOR', :numeric => '410' },
+ { :alpha2 => 'KW', :name => 'Kuwait', :alpha3 => 'KWT', :numeric => '414' },
+ { :alpha2 => 'KG', :name => 'Kyrgyzstan', :alpha3 => 'KGZ', :numeric => '417' },
+ { :alpha2 => 'LA', :name => 'Lao People\'s Democratic Republic', :alpha3 => 'LAO', :numeric => '418' },
+ { :alpha2 => 'LV', :name => 'Latvia', :alpha3 => 'LVA', :numeric => '428' },
+ { :alpha2 => 'LB', :name => 'Lebanon', :alpha3 => 'LBN', :numeric => '422' },
+ { :alpha2 => 'LS', :name => 'Lesotho', :alpha3 => 'LSO', :numeric => '426' },
+ { :alpha2 => 'LR', :name => 'Liberia', :alpha3 => 'LBR', :numeric => '430' },
+ { :alpha2 => 'LY', :name => 'Libyan Arab Jamahiriya', :alpha3 => 'LBY', :numeric => '434' },
+ { :alpha2 => 'LI', :name => 'Liechtenstein', :alpha3 => 'LIE', :numeric => '438' },
+ { :alpha2 => 'LT', :name => 'Lithuania', :alpha3 => 'LTU', :numeric => '440' },
+ { :alpha2 => 'LU', :name => 'Luxembourg', :alpha3 => 'LUX', :numeric => '442' },
+ { :alpha2 => 'MO', :name => 'Macao', :alpha3 => 'MAC', :numeric => '446' },
+ { :alpha2 => 'MK', :name => 'Macedonia, the Former Yugoslav Republic of', :alpha3 => 'MKD', :numeric => '807' },
+ { :alpha2 => 'MG', :name => 'Madagascar', :alpha3 => 'MDG', :numeric => '450' },
+ { :alpha2 => 'MW', :name => 'Malawi', :alpha3 => 'MWI', :numeric => '454' },
+ { :alpha2 => 'MY', :name => 'Malaysia', :alpha3 => 'MYS', :numeric => '458' },
+ { :alpha2 => 'MV', :name => 'Maldives', :alpha3 => 'MDV', :numeric => '462' },
+ { :alpha2 => 'ML', :name => 'Mali', :alpha3 => 'MLI', :numeric => '466' },
+ { :alpha2 => 'MT', :name => 'Malta', :alpha3 => 'MLT', :numeric => '470' },
+ { :alpha2 => 'MH', :name => 'Marshall Islands', :alpha3 => 'MHL', :numeric => '584' },
+ { :alpha2 => 'MQ', :name => 'Martinique', :alpha3 => 'MTQ', :numeric => '474' },
+ { :alpha2 => 'MR', :name => 'Mauritania', :alpha3 => 'MRT', :numeric => '478' },
+ { :alpha2 => 'MU', :name => 'Mauritius', :alpha3 => 'MUS', :numeric => '480' },
+ { :alpha2 => 'MX', :name => 'Mexico', :alpha3 => 'MEX', :numeric => '484' },
+ { :alpha2 => 'FM', :name => 'Micronesia, Federated States of', :alpha3 => 'FSM', :numeric => '583' },
+ { :alpha2 => 'MD', :name => 'Moldova, Republic of', :alpha3 => 'MDA', :numeric => '498' },
+ { :alpha2 => 'MC', :name => 'Monaco', :alpha3 => 'MCO', :numeric => '492' },
+ { :alpha2 => 'MN', :name => 'Mongolia', :alpha3 => 'MNG', :numeric => '496' },
+ { :alpha2 => 'MS', :name => 'Montserrat', :alpha3 => 'MSR', :numeric => '500' },
+ { :alpha2 => 'MA', :name => 'Morocco', :alpha3 => 'MAR', :numeric => '504' },
+ { :alpha2 => 'MZ', :name => 'Mozambique', :alpha3 => 'MOZ', :numeric => '508' },
+ { :alpha2 => 'MM', :name => 'Myanmar', :alpha3 => 'MMR', :numeric => '104' },
+ { :alpha2 => 'NA', :name => 'Namibia', :alpha3 => 'NAM', :numeric => '516' },
+ { :alpha2 => 'NR', :name => 'Nauru', :alpha3 => 'NRU', :numeric => '520' },
+ { :alpha2 => 'NP', :name => 'Nepal', :alpha3 => 'NPL', :numeric => '524' },
+ { :alpha2 => 'NL', :name => 'Netherlands', :alpha3 => 'NLD', :numeric => '528' },
+ { :alpha2 => 'AN', :name => 'Netherlands Antilles', :alpha3 => 'ANT', :numeric => '530' },
+ { :alpha2 => 'NC', :name => 'New Caledonia', :alpha3 => 'NCL', :numeric => '540' },
+ { :alpha2 => 'NZ', :name => 'New Zealand', :alpha3 => 'NZL', :numeric => '554' },
+ { :alpha2 => 'NI', :name => 'Nicaragua', :alpha3 => 'NIC', :numeric => '558' },
+ { :alpha2 => 'NE', :name => 'Niger', :alpha3 => 'NER', :numeric => '562' },
+ { :alpha2 => 'NG', :name => 'Nigeria', :alpha3 => 'NGA', :numeric => '566' },
+ { :alpha2 => 'NU', :name => 'Niue', :alpha3 => 'NIU', :numeric => '570' },
+ { :alpha2 => 'NF', :name => 'Norfolk Island', :alpha3 => 'NFK', :numeric => '574' },
+ { :alpha2 => 'MP', :name => 'Northern Mariana Islands', :alpha3 => 'MNP', :numeric => '580' },
+ { :alpha2 => 'NO', :name => 'Norway', :alpha3 => 'NOR', :numeric => '578' },
+ { :alpha2 => 'OM', :name => 'Oman', :alpha3 => 'OMN', :numeric => '512' },
+ { :alpha2 => 'PK', :name => 'Pakistan', :alpha3 => 'PAK', :numeric => '586' },
+ { :alpha2 => 'PW', :name => 'Palau', :alpha3 => 'PLW', :numeric => '585' },
+ { :alpha2 => 'PA', :name => 'Panama', :alpha3 => 'PAN', :numeric => '591' },
+ { :alpha2 => 'PG', :name => 'Papua New Guinea', :alpha3 => 'PNG', :numeric => '598' },
+ { :alpha2 => 'PY', :name => 'Paraguay', :alpha3 => 'PRY', :numeric => '600' },
+ { :alpha2 => 'PE', :name => 'Peru', :alpha3 => 'PER', :numeric => '604' },
+ { :alpha2 => 'PH', :name => 'Philippines', :alpha3 => 'PHL', :numeric => '608' },
+ { :alpha2 => 'PN', :name => 'Pitcairn', :alpha3 => 'PCN', :numeric => '612' },
+ { :alpha2 => 'PL', :name => 'Poland', :alpha3 => 'POL', :numeric => '616' },
+ { :alpha2 => 'PT', :name => 'Portugal', :alpha3 => 'PRT', :numeric => '620' },
+ { :alpha2 => 'PR', :name => 'Puerto Rico', :alpha3 => 'PRI', :numeric => '630' },
+ { :alpha2 => 'QA', :name => 'Qatar', :alpha3 => 'QAT', :numeric => '634' },
+ { :alpha2 => 'RE', :name => 'Reunion', :alpha3 => 'REU', :numeric => '638' },
+ { :alpha2 => 'RO', :name => 'Romania', :alpha3 => 'ROM', :numeric => '642' },
+ { :alpha2 => 'RU', :name => 'Russian Federation', :alpha3 => 'RUS', :numeric => '643' },
+ { :alpha2 => 'RW', :name => 'Rwanda', :alpha3 => 'RWA', :numeric => '646' },
+ { :alpha2 => 'SH', :name => 'Saint Helena', :alpha3 => 'SHN', :numeric => '654' },
+ { :alpha2 => 'KN', :name => 'Saint Kitts and Nevis', :alpha3 => 'KNA', :numeric => '659' },
+ { :alpha2 => 'LC', :name => 'Saint Lucia', :alpha3 => 'LCA', :numeric => '662' },
+ { :alpha2 => 'PM', :name => 'Saint Pierre and Miquelon', :alpha3 => 'SPM', :numeric => '666' },
+ { :alpha2 => 'VC', :name => 'Saint Vincent and the Grenadines', :alpha3 => 'VCT', :numeric => '670' },
+ { :alpha2 => 'WS', :name => 'Samoa', :alpha3 => 'WSM', :numeric => '882' },
+ { :alpha2 => 'SM', :name => 'San Marino', :alpha3 => 'SMR', :numeric => '674' },
+ { :alpha2 => 'ST', :name => 'Sao Tome and Principe', :alpha3 => 'STP', :numeric => '678' },
+ { :alpha2 => 'SA', :name => 'Saudi Arabia', :alpha3 => 'SAU', :numeric => '682' },
+ { :alpha2 => 'SN', :name => 'Senegal', :alpha3 => 'SEN', :numeric => '686' },
+ { :alpha2 => 'SC', :name => 'Seychelles', :alpha3 => 'SYC', :numeric => '690' },
+ { :alpha2 => 'SL', :name => 'Sierra Leone', :alpha3 => 'SLE', :numeric => '694' },
+ { :alpha2 => 'SG', :name => 'Singapore', :alpha3 => 'SGP', :numeric => '702' },
+ { :alpha2 => 'SK', :name => 'Slovakia', :alpha3 => 'SVK', :numeric => '703' },
+ { :alpha2 => 'SI', :name => 'Slovenia', :alpha3 => 'SVN', :numeric => '705' },
+ { :alpha2 => 'SB', :name => 'Solomon Islands', :alpha3 => 'SLB', :numeric => '090' },
+ { :alpha2 => 'SO', :name => 'Somalia', :alpha3 => 'SOM', :numeric => '706' },
+ { :alpha2 => 'ZA', :name => 'South Africa', :alpha3 => 'ZAF', :numeric => '710' },
+ { :alpha2 => 'ES', :name => 'Spain', :alpha3 => 'ESP', :numeric => '724' },
+ { :alpha2 => 'LK', :name => 'Sri Lanka', :alpha3 => 'LKA', :numeric => '144' },
+ { :alpha2 => 'SD', :name => 'Sudan', :alpha3 => 'SDN', :numeric => '736' },
+ { :alpha2 => 'SR', :name => 'Suriname', :alpha3 => 'SUR', :numeric => '740' },
+ { :alpha2 => 'SJ', :name => 'Svalbard and Jan Mayen', :alpha3 => 'SJM', :numeric => '744' },
+ { :alpha2 => 'SZ', :name => 'Swaziland', :alpha3 => 'SWZ', :numeric => '748' },
+ { :alpha2 => 'SE', :name => 'Sweden', :alpha3 => 'SWE', :numeric => '752' },
+ { :alpha2 => 'CH', :name => 'Switzerland', :alpha3 => 'CHE', :numeric => '756' },
+ { :alpha2 => 'SY', :name => 'Syrian Arab Republic', :alpha3 => 'SYR', :numeric => '760' },
+ { :alpha2 => 'TW', :name => 'Taiwan, Province of China', :alpha3 => 'TWN', :numeric => '158' },
+ { :alpha2 => 'TJ', :name => 'Tajikistan', :alpha3 => 'TJK', :numeric => '762' },
+ { :alpha2 => 'TZ', :name => 'Tanzania, United Republic of', :alpha3 => 'TZA', :numeric => '834' },
+ { :alpha2 => 'TH', :name => 'Thailand', :alpha3 => 'THA', :numeric => '764' },
+ { :alpha2 => 'TG', :name => 'Togo', :alpha3 => 'TGO', :numeric => '768' },
+ { :alpha2 => 'TK', :name => 'Tokelau', :alpha3 => 'TKL', :numeric => '772' },
+ { :alpha2 => 'TO', :name => 'Tonga', :alpha3 => 'TON', :numeric => '776' },
+ { :alpha2 => 'TT', :name => 'Trinidad and Tobago', :alpha3 => 'TTO', :numeric => '780' },
+ { :alpha2 => 'TN', :name => 'Tunisia', :alpha3 => 'TUN', :numeric => '788' },
+ { :alpha2 => 'TR', :name => 'Turkey', :alpha3 => 'TUR', :numeric => '792' },
+ { :alpha2 => 'TM', :name => 'Turkmenistan', :alpha3 => 'TKM', :numeric => '795' },
+ { :alpha2 => 'TC', :name => 'Turks and Caicos Islands', :alpha3 => 'TCA', :numeric => '796' },
+ { :alpha2 => 'TV', :name => 'Tuvalu', :alpha3 => 'TUV', :numeric => '798' },
+ { :alpha2 => 'UG', :name => 'Uganda', :alpha3 => 'UGA', :numeric => '800' },
+ { :alpha2 => 'UA', :name => 'Ukraine', :alpha3 => 'UKR', :numeric => '804' },
+ { :alpha2 => 'AE', :name => 'United Arab Emirates', :alpha3 => 'ARE', :numeric => '784' },
+ { :alpha2 => 'GB', :name => 'United Kingdom', :alpha3 => 'GBR', :numeric => '826' },
+ { :alpha2 => 'US', :name => 'United States', :alpha3 => 'USA', :numeric => '840' },
+ { :alpha2 => 'UY', :name => 'Uruguay', :alpha3 => 'URY', :numeric => '858' },
+ { :alpha2 => 'UZ', :name => 'Uzbekistan', :alpha3 => 'UZB', :numeric => '860' },
+ { :alpha2 => 'VU', :name => 'Vanuatu', :alpha3 => 'VUT', :numeric => '548' },
+ { :alpha2 => 'VE', :name => 'Venezuela', :alpha3 => 'VEN', :numeric => '862' },
+ { :alpha2 => 'VN', :name => 'Viet Nam', :alpha3 => 'VNM', :numeric => '704' },
+ { :alpha2 => 'VG', :name => 'Virgin Islands, British', :alpha3 => 'VGB', :numeric => '092' },
+ { :alpha2 => 'VI', :name => 'Virgin Islands, U.S.', :alpha3 => 'VIR', :numeric => '850' },
+ { :alpha2 => 'WF', :name => 'Wallis and Futuna', :alpha3 => 'WLF', :numeric => '876' },
+ { :alpha2 => 'EH', :name => 'Western Sahara', :alpha3 => 'ESH', :numeric => '732' },
+ { :alpha2 => 'YE', :name => 'Yemen', :alpha3 => 'YEM', :numeric => '887' },
+ { :alpha2 => 'ZM', :name => 'Zambia', :alpha3 => 'ZMB', :numeric => '894' },
+ { :alpha2 => 'ZW', :name => 'Zimbabwe', :alpha3 => 'ZWE', :numeric => '716' }
+ ]
+
+ def self.find(name)
+ raise InvalidCountryCodeError, "Cannot lookup country for an empty name" if name.blank?
+
+ case name.length
+ when 2, 3
+ upcase_name = name.upcase
+ country_code = CountryCode.new(name)
+ country = COUNTRIES.detect{|c| c[country_code.format] == upcase_name }
+ else
+ country = COUNTRIES.detect{|c| c[:name] == name }
+ end
+ raise InvalidCountryCodeError, "No country could be found for the country #{name}" if country.nil?
+ Country.new(country.dup)
+ end
+ end
+end
4 lib/active_fulfillment/lib/error.rb
@@ -0,0 +1,4 @@
+module ActiveMerchant #:nodoc:
+ class ActiveMerchantError < StandardError #:nodoc:
+ end
+end
22 lib/active_fulfillment/lib/post_data.rb
@@ -0,0 +1,22 @@
+require 'cgi'
+
+class PostData < Hash
+ class_inheritable_accessor :required_fields, :instance_writer => false
+ self.required_fields = []
+
+ def []=(key, value)
+ return if value.blank? && !required?(key)
+ super
+ end
+
+ def to_post_data
+ collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
+ end
+
+ alias_method :to_s, :to_post_data
+
+ private
+ def required?(key)
+ required_fields.include?(key)
+ end
+end
80 lib/active_fulfillment/lib/posts_data.rb
@@ -0,0 +1,80 @@
+module ActiveMerchant #:nodoc:
+ class ConnectionError < ActiveMerchantError
+ end
+
+ class RetriableConnectionError < ConnectionError
+ end
+
+ module PostsData #:nodoc:
+ MAX_RETRIES = 3
+ OPEN_TIMEOUT = 60
+ READ_TIMEOUT = 60
+
+ def self.included(base)
+ base.class_inheritable_accessor :ssl_strict
+ base.ssl_strict = true
+
+ base.class_inheritable_accessor :pem_password
+ base.pem_password = false
+
+ base.class_inheritable_accessor :retry_safe
+ base.retry_safe = false
+ end
+
+ def ssl_post(url, data, headers = {})
+ uri = URI.parse(url)
+
+ http = Net::HTTP.new(uri.host, uri.port)
+ http.open_timeout = OPEN_TIMEOUT
+ http.read_timeout = READ_TIMEOUT
+ http.use_ssl = true
+
+ if ssl_strict
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ http.ca_file = File.dirname(__FILE__) + '/../../certs/cacert.pem'
+ else
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ end
+
+ if @options && !@options[:pem].blank?
+ http.cert = OpenSSL::X509::Certificate.new(@options[:pem])
+
+ if pem_password
+ raise ArgumentError, "The private key requires a password" if @options[:pem_password].blank?
+ http.key = OpenSSL::PKey::RSA.new(@options[:pem], @options[:pem_password])
+ else
+ http.key = OpenSSL::PKey::RSA.new(@options[:pem])
+ end
+ end
+
+ retry_exceptions do
+ begin
+ http.post(uri.request_uri, data, headers).body
+ rescue EOFError => e
+ raise ConnectionError, "The remote server dropped the connection"
+ rescue Errno::ECONNRESET => e
+ raise ConnectionError, "The remote server reset the connection"
+ rescue Errno::ECONNREFUSED => e
+ raise RetriableConnectionError, "The remote server refused the connection"
+ rescue Timeout::Error, Errno::ETIMEDOUT => e
+ raise ConnectionError, "The connection to the remote server timed out"
+ end
+ end
+ end
+
+ def retry_exceptions
+ retries = MAX_RETRIES
+ begin
+ yield
+ rescue RetriableConnectionError => e
+ retries -= 1
+ retry unless retries.zero?
+ raise ConnectionError, e.message
+ rescue ConnectionError
+ retries -= 1
+ retry if retry_safe && !retries.zero?
+ raise
+ end
+ end
+ end
+end
16 lib/active_fulfillment/lib/requires_parameters.rb
@@ -0,0 +1,16 @@
+module ActiveMerchant #:nodoc:
+ module RequiresParameters #:nodoc:
+ def requires!(hash, *params)
+ params.each do |param|
+ if param.is_a?(Array)
+ raise ArgumentError.new("Missing required parameter: #{param.first}") unless hash.has_key?(param.first)
+
+ valid_options = param[1..-1]
+ raise ArgumentError.new("Parameter: #{param.first} must be one of #{valid_options.to_sentence(:connector => 'or')}") unless valid_options.include?(hash[param.first])
+ else
+ raise ArgumentError.new("Missing required parameter: #{param}") unless hash.has_key?(param)
+ end
+ end
+ end
+ end
+end
76 lib/active_fulfillment/lib/validateable.rb
@@ -0,0 +1,76 @@
+module ActiveMerchant #:nodoc:
+ module Validateable #:nodoc:
+ def valid?
+ errors.clear
+
+ before_validate if respond_to?(:before_validate, true)
+ validate if respond_to?(:validate, true)
+
+ errors.empty?
+ end
+
+ def initialize(attributes = {})
+ self.attributes = attributes
+ end
+
+ def errors
+ @errors ||= Errors.new(self)
+ end
+
+ private
+
+ def attributes=(attributes)
+ unless attributes.nil?
+ for key, value in attributes
+ send("#{key}=", value )
+ end
+ end
+ end
+
+ # This hash keeps the errors of the object
+ class Errors < HashWithIndifferentAccess
+
+ def initialize(base)
+ @base = base
+ end
+
+ def count
+ size
+ end
+
+ # returns a specific fields error message.
+ # if more than one error is available we will only return the first. If no error is available
+ # we return an empty string
+ def on(field)
+ self[field].to_a.first
+ end
+
+ def add(field, error)
+ self[field] ||= []
+ self[field] << error
+ end
+
+ def add_to_base(error)
+ add(:base, error)
+ end
+
+ def each_full
+ full_messages.each { |msg| yield msg }
+ end
+
+ def full_messages
+ result = []
+
+ self.each do |key, messages|
+ if key == 'base'
+ result << "#{messages.first}"
+ else
+ result << "#{key.to_s.humanize} #{messages.first}"
+ end
+ end
+
+ result
+ end
+ end
+ end
+end
7,815 lib/certs/cacert.pem
7,815 additions, 0 deletions not shown
11 test/fixtures.yml
@@ -0,0 +1,11 @@
+shipwire:
+ login:
+ password:
+
+webgistix:
+ login:
+ password:
+
+amazon:
+ login:
+ password:
62 test/remote/amazon_test.rb
@@ -0,0 +1,62 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RemoteAmazonTest < Test::Unit::TestCase
+
+ # In order for these tests to work you must have a live account with Amazon.
+ # You can sign up at http://amazonservices.com/fulfillment/
+ # The SKUs must also exist in your inventory. You do not want the SKUs you
+ # use for testing to actually have inventory in the Amazon warehouse, or else
+ # the shipments will actually be fulfillable
+ def setup
+ @service = AmazonService.new( fixtures(:amazon) )
+
+ @options = {
+ :shipping_method => 'Standard',
+ :order_date => Time.now.utc.yesterday,
+ :comment => "Delayed due to tornados"
+ }
+
+ @address = { :name => 'Johnny Chase',
+ :address1 => '100 Information Super Highway',
+ :address2 => 'Suite 66',
+ :city => 'Beverly Hills',
+ :state => 'CA',
+ :country => 'US',
+ :zip => '90210'
+ }
+
+ @line_items = [
+ { :sku => 'SETTLERS8',
+ :quantity => 1 #,
+ #:comment => 'Awesome'
+ }
+ ]
+ end
+
+ def test_successful_order_submission
+ response = @service.fulfill(generate_order_id, @address, @line_items, @options)
+ assert response.success?
+ assert !response.test?
+ end
+
+ def test_order_multiple_line_items
+ @line_items.push(
+ { :sku => 'CARCASSONNE',
+ :quantity => 2
+ }
+ )
+
+ response = @service.fulfill(generate_order_id, @address, @line_items, @options)
+ assert response.success?
+ end
+
+ def test_invalid_credentials_during_fulfillment
+ service = AmazonService.new(
+ :login => 'y',
+ :password => 'p')
+
+ response = service.fulfill(generate_order_id, @address, @line_items, @options)
+ assert !response.success?
+ assert_equal "AWS was not able to validate the provided access credentials.", response.message
+ end
+end
124 test/remote/shipwire_test.rb
@@ -0,0 +1,124 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RemoteShipwireTest < Test::Unit::TestCase
+ def setup
+ Base.mode = :test
+
+ @shipwire = ShipwireService.new( fixtures(:shipwire) )
+
+ @options = {
+ :warehouse => '01',
+ :shipping_method => 'UPS Ground'
+ }
+
+ @address = { :name => 'Fred Brooks',
+ :address1 => '1234 Penny Lane',
+ :city => 'Jonsetown',
+ :state => 'NC',
+ :country => 'US',
+ :zip => '23456',
+ :email => 'buyer@jadedpallet.com'
+ }
+
+ @line_items = [
+ { :sku => '9999',
+ :quantity => 25,
+ :description => 'Libtech Snowboard',
+ :length => 3,
+ :width => 2,
+ :height => 1,
+ :weight => 2,
+ :declared_value => 1.25
+ }
+ ]
+ end
+
+ def test_invalid_credentials_during_fulfillment
+ shipwire = ShipwireService.new(
+ :login => 'your@email.com',
+ :password => 'password')
+
+ response = shipwire.fulfill('123456', @address, @line_items, @options)
+ assert !response.success?
+ assert response.test?
+ assert_equal 'Error', response.params['status']
+ assert_equal "Could not verify e-mail/password combination", response.message
+ end
+
+ def test_successful_order_submission
+ response = @shipwire.fulfill('123456', @address, @line_items, @options)
+ assert response.success?
+ assert response.test?
+ assert response.params['transaction_id']
+ assert_equal '1', response.params['total_orders']
+ assert_equal '1', response.params['total_items']
+ assert_equal '0', response.params['status']
+ assert_equal 'Successfully submitted the order', response.message
+ end
+
+ def test_order_multiple_line_items
+ @line_items.push(
+ { :sku => '9998',
+ :quantity => 25,
+ :description => 'Libtech Snowboard',
+ :length => 3,
+ :width => 2,
+ :height => 1,
+ :weight => 2,
+ :value => 1.25
+ }
+ )
+
+ response = @shipwire.fulfill('123456', @address, @line_items, @options)
+ assert response.success?
+ assert response.test?
+ assert response.params['transaction_id']
+ assert_equal '1', response.params['total_orders']
+ assert_equal '2', response.params['total_items']
+ assert_equal '0', response.params['status']
+ assert_equal 'Successfully submitted the order', response.message
+ end
+
+ def test_no_sku_is_sent_with_fulfillment
+ options = {
+ :shipping_method => 'UPS Ground'
+ }
+
+ line_items = [
+ { :quantity => 1,
+ :description => 'Libtech Snowboard'
+ }
+ ]
+
+ response = @shipwire.fulfill('123456', @address, line_items, options)
+
+ assert response.success?
+ assert response.test?
+ assert_not_nil response.params['transaction_id']
+ assert_equal "1", response.params['total_orders']
+ assert_equal "0", response.params['total_items']
+ assert_equal "0", response.params['status']
+ assert_equal 'Successfully submitted the order', response.message
+ end
+
+ def test_invalid_credentials_during_inventory
+ shipwire = ShipwireService.new(
+ :login => 'your@email.com',
+ :password => 'password')
+
+ response = shipwire.fetch_stock_levels
+ assert !response.success?
+ assert response.test?
+ assert_equal 'Error', response.params['status']
+ assert_equal "Error with EmailAddress, valid email is required. There is an error in XML document.", response.message
+ end
+
+ def test_get_inventory
+ response = @shipwire.fetch_stock_levels
+ assert response.success?
+ assert response.test?
+ assert_equal 14, response.stock_levels["GD802-024"]
+ assert_equal 32, response.stock_levels["GD201-500"]
+ assert_equal "2", response.params["total_products"]
+ end
+end
59 test/remote/webgistix_test.rb
@@ -0,0 +1,59 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RemoteWebgistixTest < Test::Unit::TestCase
+ def setup
+ Base.mode = :test
+
+ @service = WebgistixService.new( fixtures(:webgistix) )
+
+ @options = {
+ :shipping_method => 'Ground'
+ }
+
+ @address = { :name => 'Fred Brooks',
+ :address1 => '1234 Penny Lane',
+ :city => 'Jonsetown',
+ :state => 'NC',
+ :country => 'US',
+ :zip => '23456',
+ :email => 'buyer@jadedpallet.com'
+ }
+
+ @line_items = [
+ { :sku => 'testitem',
+ :quantity => 2
+ }
+ ]
+ end
+
+ def test_successful_order_submission
+ response = @service.fulfill('123456', @address, @line_items, @options)
+ assert response.success?
+ assert response.test?
+ assert_equal WebgistixService::SUCCESS_MESSAGE, response.message
+ end
+
+ def test_order_multiple_line_items
+ @line_items.push(
+ { :sku => '9998',
+ :quantity => 25
+ }
+ )
+
+ response = @service.fulfill('123456', @address, @line_items, @options)
+ assert response.success?
+ assert response.test?
+ assert_equal WebgistixService::SUCCESS_MESSAGE, response.message
+ end
+
+ def test_invalid_credentials_during_fulfillment
+ service = WebgistixService.new(
+ :login => 'your@email.com',
+ :password => 'password')
+
+ response = service.fulfill('123456', @address, @line_items, @options)
+ assert !response.success?
+ assert response.test?
+ assert_equal WebgistixService::FAILURE_MESSAGE, response.message
+ end
+end
60 test/test_helper.rb
@@ -0,0 +1,60 @@
+#!/usr/bin/env ruby
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+
+require 'test/unit'
+require 'digest/md5'
+require 'active_fulfillment'
+
+begin
+ require 'mocha'
+rescue LoadError
+ require 'rubygems'
+ require 'mocha'
+end
+
+module Test
+ module Unit
+ class TestCase
+ include ActiveMerchant::Fulfillment
+
+ LOCAL_CREDENTIALS = ENV['HOME'] + '/.active_merchant/fixtures.yml' unless defined?(LOCAL_CREDENTIALS)
+ DEFAULT_CREDENTIALS = File.dirname(__FILE__) + '/fixtures.yml' unless defined?(DEFAULT_CREDENTIALS)
+
+ def generate_order_id
+ md5 = Digest::MD5.new
+ now = Time.now
+ md5 << now.to_s
+ md5 << String(now.usec)
+ md5 << String(rand(0))
+ md5 << String($$)
+ md5 << self.class.name
+ md5.hexdigest
+ end
+
+ def all_fixtures
+ @@fixtures ||= load_fixtures
+ end
+
+ def fixtures(key)
+ data = all_fixtures[key] || raise(StandardError, "No fixture data was found for '#{key}'")
+
+ data.dup
+ end
+
+ def load_fixtures
+ file = File.exists?(LOCAL_CREDENTIALS) ? LOCAL_CREDENTIALS : DEFAULT_CREDENTIALS
+ yaml_data = YAML.load(File.read(file))
+ symbolize_keys(yaml_data)
+
+ yaml_data
+ end
+
+ def symbolize_keys(hash)
+ return unless hash.is_a?(Hash)
+
+ hash.symbolize_keys!
+ hash.each{|k,v| symbolize_keys(v)}
+ end
+ end
+ end
+end
17 test/unit/base_test.rb
@@ -0,0 +1,17 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class BaseTest < Test::Unit::TestCase
+ include ActiveMerchant::Fulfillment
+
+ def test_get_shipwire_by_string
+ assert_equal ShipwireService, Base.service('shipwire')
+ end
+
+ def test_get_shipwire_by_name
+ assert_equal ShipwireService, Base.service(:shipwire)
+ end
+
+ def test_get_unknown_service
+ assert_raise(NameError){ Base.service(:polar_north) }
+ end
+end
193 test/unit/services/amazon_test.rb
@@ -0,0 +1,193 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+
+class AmazonTest < Test::Unit::TestCase
+ def setup
+ @service = AmazonService.new(
+ :login => 'l',
+ :password => 'p'
+ )
+
+ @options = {
+ :shipping_method => 'Standard',
+ :order_date => Time.now.utc.yesterday,
+ :comment => "Delayed due to tornados"
+ }
+
+ @address = { :name => 'Johnny Chase',
+ :address1 => '100 Information Super Highway',
+ :address2 => 'Suite 66',
+ :city => 'Beverly Hills',
+ :state => 'CA',
+ :country => 'US',
+ :zip => '90210'
+ }
+
+ @line_items = [
+ { :sku => 'SETTLERS1',
+ :quantity => 1,
+ :comment => 'Awesome'
+ }
+ ]
+ end
+
+ def test_successful_fulfillment
+ @service.expects(:ssl_post).returns(successful_fulfillment_response)
+ response = @service.fulfill('12345678', @address, @line_items, @options)
+ assert response.success?
+ end
+
+ def test_invalid_arguments
+ @service.expects(:ssl_post).returns(invalid_arguments_response)
+ response = @service.fulfill('12345678', @address, @line_items, @options)
+ assert !response.success?
+ assert_equal '1 error: The Displayable Order Comment value cannot be blank[null].', response.params['response_comment']
+ end
+
+ def test_missing_order_comment
+ @options.delete(:comment)
+ assert_raise(ArgumentError) { @service.fulfill('12345678', @address, @line_items, @options) }
+ end
+
+ def test_missing_order_date
+ @options.delete(:order_date)
+ assert_raise(ArgumentError) { @service.fulfill('12345678', @address, @line_items, @options) }
+ end
+
+ def test_missing_shipping_method
+ @options.delete(:shipping_method)
+ assert_raise(ArgumentError) { @service.fulfill('12345678', @address, @line_items, @options) }
+ end
+
+ def test_404_error
+ @service.expects(:ssl_post).returns(response_from_404)
+ response = @service.fulfill('12345678', @address, @line_items, @options)
+ assert !response.success?
+ assert_equal AmazonService::MESSAGES[:error], response.message
+ end
+
+ def test_soap_fault
+ @service.expects(:ssl_post).returns(internal_error_response)
+ response = @service.fulfill('12345678', @address, @line_items, @options)
+ assert !response.success?
+ assert_equal 'aws:Server.InternalError', response.params['faultcode']
+ assert_equal 'We encountered an internal error. Please try again.', response.params['faultstring']
+ assert_equal 'We encountered an internal error. Please try again.', response.message
+ end
+
+ private
+ def response_for_empty_request
+ '<ns:GetErrorResponse xmlns:ns="http://xino.amazonaws.com/doc/"><ns:Error><ns:Code>MissingDateHeader</ns:Code><ns:Message>Authorized request must have a "date" or "x-amz-date" header.</ns:Message></ns:Error><ns:RequestID>79ceaffe-e5a3-46a5-b36a-9ce958d68939</ns:RequestID></ns:GetErrorResponse>'
+ end
+
+ def failed_login_response
+ <<-XML
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:aws="http://webservices.amazon.com/AWSFault/2005-15-09"><env:Body><env:Fault><faultcode>aws:Client.InvalidAccessKeyId</faultcode><faultstring>AWS was not able to validate the provided access credentials.</faultstring><detail><aws:RequestId xmlns:aws="http://webservices.amazon.com/AWSFault/2005-15-09">9f4d8239-c274-4248-9b9f-48faef163627</aws:RequestId></detail></env:Fault></env:Body></env:Envelope>
+ XML
+ end
+
+ def internal_error_response
+ <<-XML
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:aws="http://webservices.amazon.com/AWSFault/2005-15-09"><env:Body><env:Fault><faultcode>aws:Server.InternalError</faultcode><faultstring>We encountered an internal error. Please try again.</faultstring><detail><aws:RequestId xmlns:aws="http://webservices.amazon.com/AWSFault/2005-15-09">bd9ff2d1-2ad6-4991-8eea-01267385c704</aws:RequestId></detail></env:Fault></env:Body></env:Envelope>
+ XML
+ end
+
+ def response_from_404
+ '<html><head><title>Apache Tomcat/5.5.9 - Error report</title><style><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> </head><body><h1>HTTP Status 404 - Servlet XinoServlet is not available</h1><HR size="1" noshade="noshade"><p><b>type</b> Status report</p><p><b>message</b> <u>Servlet XinoServlet is not available</u></p><p><b>description</b> <u>The requested resource (Servlet XinoServlet is not available) is not available.</u></p><HR size="1" noshade="noshade"><h3>Apache Tomcat/5.5.9</h3></body></html>'
+ end
+
+ def successful_fulfillment_response
+ <<-XML
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+ <env:Body>
+ <ns1:CreateFulfillmentOrderResponse xmlns:ns1="http://fulfillment.amazonaws.com/doc/FulfillmentService/2006-12-12">
+ <ns1:CreateFulfillmentOrderResult>
+ <ns1:Response>
+ <ns1:ResponseStatus>Accepted</ns1:ResponseStatus>
+ </ns1:Response>
+ <ns1:Request>
+ <ns1:CreateFulfillmentOrderRequest>
+ <ns1:Credentials>
+ <ns1:Email>merchant@example.com</ns1:Email>
+ <ns1:Password>password</ns1:Password>
+ </ns1:Credentials>
+ <ns1:MerchantFulfillmentOrderId>123456</ns1:MerchantFulfillmentOrderId>
+ <ns1:DisplayableOrderId>123456</ns1:DisplayableOrderId>
+ <ns1:DisplayableOrderDate>2007-10-21T20:15:28Z</ns1:DisplayableOrderDate>
+ <ns1:DisplayableOrderComment>Delayed due to tornados</ns1:DisplayableOrderComment>
+ <ns1:DeliverySLA>Standard</ns1:DeliverySLA>
+ <ns1:DestinationAddress>
+ <ns1:Name>Jaded Pixel Technologies</ns1:Name>
+ <ns1:Line1>5 Elm St.</ns1:Line1>
+ <ns1:Line2>#500</ns1:Line2>
+ <ns1:City>Beverly Hills</ns1:City>
+ <ns1:StateOrRegion>CA</ns1:StateOrRegion>
+ <ns1:CountryCode>US</ns1:CountryCode>
+ <ns1:PostalCode>90210</ns1:PostalCode>
+ </ns1:DestinationAddress>
+ <ns1:Items>
+ <ns1:MerchantSKU>SETTLERS</ns1:MerchantSKU>
+ <ns1:MerchantFulfillmentOrderItemId>0</ns1:MerchantFulfillmentOrderItemId>
+ <ns1:Quantity>1</ns1:Quantity>
+ <ns1:DisplayableComment>Awesome</ns1:DisplayableComment>
+ </ns1:Items>
+ </ns1:CreateFulfillmentOrderRequest>
+ <ns1:IsValid>True</ns1:IsValid>
+ </ns1:Request>
+ </ns1:CreateFulfillmentOrderResult>
+ </ns1:CreateFulfillmentOrderResponse>
+ </env:Body>
+</env:Envelope>
+ XML
+ end
+
+ def invalid_arguments_response
+ <<-XML
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+ <env:Body>
+ <ns1:CreateFulfillmentOrderResponse xmlns:ns1="http://fulfillment.amazonaws.com/doc/FulfillmentService/2006-12-12">
+ <ns1:CreateFulfillmentOrderResult>
+ <ns1:Response>
+ <ns1:ResponseStatus>InvalidArguments</ns1:ResponseStatus>
+ <ns1:ResponseComment>1 error:
+The Displayable Order Comment value cannot be blank[null].
+</ns1:ResponseComment>
+ </ns1:Response>
+ <ns1:Request>
+ <ns1:CreateFulfillmentOrderRequest>
+ <ns1:Credentials>
+ <ns1:Email>merchant@example.com</ns1:Email>
+ <ns1:Password>password</ns1:Password>
+ </ns1:Credentials>
+ <ns1:MerchantFulfillmentOrderId>12345678</ns1:MerchantFulfillmentOrderId>
+ <ns1:DisplayableOrderId>12345678</ns1:DisplayableOrderId>
+ <ns1:DisplayableOrderDate>2007-10-21T21:10:48Z</ns1:DisplayableOrderDate>
+ <ns1:DeliverySLA>Standard</ns1:DeliverySLA>
+ <ns1:DestinationAddress>
+ <ns1:Name>Jaded Pixel Technologies</ns1:Name>
+ <ns1:Line1>5 Elm St.</ns1:Line1>
+ <ns1:Line2>#500</ns1:Line2>
+ <ns1:City>Beverly Hills</ns1:City>
+ <ns1:StateOrRegion>CA</ns1:StateOrRegion>
+ <ns1:CountryCode>US</ns1:CountryCode>
+ <ns1:PostalCode>90210</ns1:PostalCode>
+ </ns1:DestinationAddress>
+ <ns1:Items>
+ <ns1:MerchantSKU>SETTLERS1</ns1:MerchantSKU>
+ <ns1:MerchantFulfillmentOrderItemId>0</ns1:MerchantFulfillmentOrderItemId>
+ <ns1:Quantity>1</ns1:Quantity>
+ <ns1:DisplayableComment>Awesome</ns1:DisplayableComment>
+ </ns1:Items>
+ </ns1:CreateFulfillmentOrderRequest>
+ <ns1:IsValid>True</ns1:IsValid>
+ </ns1:Request>
+ </ns1:CreateFulfillmentOrderResult>
+ </ns1:CreateFulfillmentOrderResponse>
+ </env:Body>
+</env:Envelope>
+ XML
+ end
+end
77 test/unit/services/shipwire_test.rb
@@ -0,0 +1,77 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+
+class ShipwireTest < Test::Unit::TestCase
+ def setup
+ @shipwire = ShipwireService.new(
+ :login => 'cody@example.com',
+ :password => 'test'
+ )
+
+ @options = {
+ :warehouse => '01',
+ :shipping_method => 'UPS Ground'
+ }
+
+ @address = { :name => 'Fred Brooks',
+ :address1 => '1234 Penny Lane',
+ :city => 'Jonsetown',
+ :state => 'NC',
+ :country => 'US',
+ :zip => '23456',
+ :email => 'buyer@jadedpallet.com'
+ }
+
+ @line_items = [
+ { :sku => '9999',
+ :quantity => 25,
+ :description => 'Libtech Snowboard',
+ :length => 3,
+ :width => 2,
+ :height => 1,
+ :weight => 2,
+ :declared_value => 1.25
+ }
+ ]
+ end
+
+ def test_missing_login
+ assert_raise(ArgumentError) do
+ ShipwireService.new(:password => 'test')
+ end
+ end
+
+ def test_missing_password
+ assert_raise(ArgumentError) do
+ ShipwireService.new(:login => 'cody')
+ end
+ end
+
+ def test_missing_credentials
+ assert_raise(ArgumentError) do
+ ShipwireService.new(:password => 'test')
+ end
+ end
+
+ def test_credentials_present
+ assert_nothing_raised do
+ ShipwireService.new(
+ :login => 'cody',
+ :password => 'test'
+ )
+ end
+ end
+
+ def test_country_format
+ xml = REXML::Document.new(@shipwire.send(:build_fulfillment_request, '123456', @address, @line_items, @options))
+ country_node = REXML::XPath.first(xml, "//Country")
+ assert_equal 'US United States', country_node.text
+ end
+
+ def test_england_country_format
+ @address[:country] = 'GB'
+
+ xml = REXML::Document.new(@shipwire.send(:build_fulfillment_request, '123456', @address, @line_items, @options))
+ country_node = REXML::XPath.first(xml, "//Country")
+ assert_equal 'UK United Kingdom', country_node.text
+ end
+end
135 test/unit/services/webgistix_test.rb
@@ -0,0 +1,135 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+
+class WebgistixTest < Test::Unit::TestCase
+ def setup
+ Base.mode = :test
+
+ @service = WebgistixService.new(
+ :login => 'cody@example.com',
+ :password => 'test'
+ )
+
+ @options = {
+ :shipping_method => 'UPS Ground'
+ }
+
+ @address = { :name => 'Fred Brooks',
+ :address1 => '1234 Penny Lane',
+ :city => 'Jonsetown',
+ :state => 'NC',
+ :country => 'US',
+ :zip => '23456',
+ :email => 'buyer@jadedpallet.com'
+ }
+
+ @line_items = [
+ { :sku => '9999',
+ :quantity => 25
+ }
+ ]
+ end
+
+ def test_missing_login
+ assert_raise(ArgumentError) do
+ WebgistixService.new(:password => 'test')
+ end
+ end
+
+ def test_missing_password
+ assert_raise(ArgumentError) do
+ WebgistixService.new(:login => 'cody')
+ end
+ end
+
+ def test_missing_credentials
+ assert_raise(ArgumentError) do
+ WebgistixService.new(:password => 'test')
+ end
+ end
+
+ def test_credentials_present
+ assert_nothing_raised do
+ WebgistixService.new(
+ :login => 'cody',
+ :password => 'test'
+ )
+ end
+ end
+
+ def test_successful_fulfillment
+ @service.expects(:ssl_post).returns(successful_response)
+
+ response = @service.fulfill('123456', @address, @line_items, @options)
+ assert response.success?
+ assert response.test?
+ assert_equal WebgistixService::SUCCESS_MESSAGE, response.message
+ assert_equal '619669', response.params['order_id']
+ end
+
+ def test_minimal_successful_fulfillment
+ @service.expects(:ssl_post).returns(minimal_successful_response)
+
+ response = @service.fulfill('123456', @address, @line_items, @options)
+ assert response.success?
+ assert response.test?
+ assert_equal WebgistixService::SUCCESS_MESSAGE, response.message
+ assert_nil response.params['order_id']
+ end
+
+ def test_failed_fulfillment
+ @service.expects(:ssl_post).returns(failure_response)
+
+ response = @service.fulfill('123456', @address, @line_items, @options)
+ assert !response.success?
+ assert response.test?
+ assert_equal WebgistixService::FAILURE_MESSAGE, response.message
+ assert_nil response.params['order_id']
+
+ assert_equal 'No Address Line 1', response.params['error_0']
+ assert_equal 'Unknown ItemID: testitem', response.params['error_1']
+ assert_equal 'Unknown ItemID: WX-01-1000', response.params['error_2']
+ end
+
+ def test_failed_login
+ @service.expects(:ssl_post).returns(invalid_login_response)
+
+ response = @service.fulfill('123456', @address, @line_items, @options)
+ assert !response.success?
+ assert response.test?
+ assert_equal WebgistixService::FAILURE_MESSAGE, response.message
+ assert_nil response.params['order_id']
+