Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

first commit

  • Loading branch information...
commit 0eebee7474e06c90ab3842e5e530837fbc93f9a7 0 parents
@jamesmacaulay jamesmacaulay authored
Showing with 10,955 additions and 0 deletions.
  1. +5 −0 .gitignore
  2. +20 −0 MIT-LICENSE
  3. +121 −0 README.markdown
  4. +49 −0 Rakefile
  5. +1 −0  init.rb
  6. +44 −0 lib/active_shipping.rb
  7. +297 −0 lib/active_shipping/lib/country.rb
  8. +83 −0 lib/active_shipping/lib/posts_data.rb
  9. +17 −0 lib/active_shipping/lib/requires_parameters.rb
  10. +15 −0 lib/active_shipping/shipping/base.rb
  11. +73 −0 lib/active_shipping/shipping/carrier.rb
  12. +15 −0 lib/active_shipping/shipping/carriers.rb
  13. +16 −0 lib/active_shipping/shipping/carriers/bogus_carrier.rb
  14. +260 −0 lib/active_shipping/shipping/carriers/ups.rb
  15. +375 −0 lib/active_shipping/shipping/carriers/usps.rb
  16. +92 −0 lib/active_shipping/shipping/location.rb
  17. +134 −0 lib/active_shipping/shipping/package.rb
  18. +52 −0 lib/active_shipping/shipping/rate_estimate.rb
  19. +55 −0 lib/active_shipping/shipping/response.rb
  20. +7,815 −0 lib/certs/cacert.pem
  21. +13 −0 lib/vendor/test_helper.rb
  22. +36 −0 lib/vendor/xml_node/README
  23. +21 −0 lib/vendor/xml_node/Rakefile
  24. +32 −0 lib/vendor/xml_node/benchmark/bench_generation.rb
  25. +1 −0  lib/vendor/xml_node/init.rb
  26. +220 −0 lib/vendor/xml_node/lib/xml_node.rb
  27. +94 −0 lib/vendor/xml_node/test/test_generating.rb
  28. +43 −0 lib/vendor/xml_node/test/test_parsing.rb
  29. +6 −0 test/fixtures.yml
  30. +85 −0 test/fixtures/xml/usps/beverly_hills_to_ottawa_book_rate_response.xml
  31. +168 −0 test/fixtures/xml/usps/beverly_hills_to_ottawa_book_wii_rate_response.xml
  32. +85 −0 test/fixtures/xml/usps/beverly_hills_to_ottawa_wii_rate_response.xml
  33. +76 −0 test/remote/ups_test.rb
  34. +123 −0 test/remote/usps_test.rb
  35. +110 −0 test/test_helper.rb
  36. +18 −0 test/unit/base_test.rb
  37. +19 −0 test/unit/carriers/ups_test.rb
  38. +130 −0 test/unit/carriers/usps_test.rb
  39. +44 −0 test/unit/location_test.rb
  40. +74 −0 test/unit/package_test.rb
  41. +18 −0 test/unit/response_test.rb
5 .gitignore
@@ -0,0 +1,5 @@
+.DS_Store
+
+*.orig
+
+.dotest
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2008 James MacAulay
+
+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 PURPOa AND
+NONINFRINGEMENT. IN NO EVENT SaALL 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.
121 README.markdown
@@ -0,0 +1,121 @@
+# Active Shipping
+
+This library is meant to interface with the web services of various shipping carriers. The goal is to abstract the features that are most frequently used into a pleasant and consistent Ruby API. Active Shipping is an extension of [Active Merchant][], and as such, it borrows heavily from conventions used in the latter.
+
+We are starting out by only implementing the ability to list available shipping rates for a particular origin, destination, and set of packages. Further development could take advantage of other common features of carriers' web services such as tracking orders and printing labels.
+
+Active Shipping is currently being used and improved in a production environment for the e-commerce application [Shopify][]. Development is being done by [James MacAulay][] (<james@jadedpixel.com>). Discussion is welcome in the [Active Merchant Google Group][discuss].
+
+[Active Merchant]:http://www.activemerchant.org
+[Shopify]:http://www.shopify.com
+[James MacAulay]:http://jmacaulay.net
+[discuss]:http://groups.google.com/group/activemerchant
+
+## Supported Shipping Carriers
+
+* [UPS](http://www.ups.com)
+* [USPS](http://www.usps.com)
+* more soon!
+
+## Prerequisites
+
+* [active_support](http://github.com/rails/rails/tree/master/activesupport)
+* [xml_node](http://github.com/tobi/xml_node/) (right now a version of it is actually included in this library, so you don't need to worry about it yet)
+* [mocha](http://mocha.rubyforge.org/) for the tests
+
+## Download & Installation
+
+Currently this library is available on GitHub:
+
+ <http://github.com/jamesmacaulay/active_shipping>
+
+You will need to get [Git][] if you don't have it. Then:
+
+ > git clone git://github.com/jamesmacaulay/active_shipping.git
+
+Active Shipping includes an init.rb file. This means that Rails will automatically load it on startup. Check out [git-archive][] for exporting the file tree from your repository to your vendor directory.
+
+Gem and tarball forthcoming on rubyforge.
+
+[Git]:http://git.or.cz/
+[git-archive]:http://www.kernel.org/pub/software/scm/git/docs/git-archive.html
+
+## Sample Usage
+
+ require 'active_shipping'
+ include ActiveMerchant::Shipping
+
+ # Package up a poster and a Wii for your nephew.
+ packages = [
+ Package.new( 100, # 100 grams
+ [93,10], # 93 cm long, 10 cm diameter
+ :cylinder => true), # cylinders have different volume calculations
+
+ Package.new( (7.5 * 16), # 7.5 lbs, times 16 oz/lb.
+ [15, 10, 4.5], # 15x10x4.5 inches
+ :units => :imperial) # not grams, not centimetres
+ ]
+
+ # You live in Beverly Hills, he lives in Ottawa
+ origin = Location.new( :country => 'US',
+ :state => 'CA',
+ :city => 'Beverly Hills',
+ :zip => '90210')
+
+ destination = Location.new( :country => 'CA',
+ :province => 'ON',
+ :city => 'Ottawa',
+ :postal_code => 'K1P 1J1')
+
+ # Find out how much it'll be.
+ ups = UPS.new(:login => 'auntjudy', :password => 'secret', :key => 'xml-access-key')
+ response = ups.find_rates(origin, destination, packages)
+
+ ups_rates = response.rates.sort_by(&:price).collect {|rate| [rate.service_name, rate.price]}
+ # => [["UPS Standard", 3936],
+ # ["UPS Worldwide Expedited", 8682],
+ # ["UPS Saver", 9348],
+ # ["UPS Express", 9702],
+ # ["UPS Worldwide Express Plus", 14502]]
+
+ # Check out USPS for comparison...
+ usps = USPS.new(:login => 'developer-key')
+ response = usps.find_rates(origin, destination, packages)
+
+ usps_rates = response.rates.sort_by(&:price).collect {|rate| [rate.service_name, rate.price]}
+ # => [["USPS Priority Mail International", 4110],
+ # ["USPS Express Mail International (EMS)", 5750],
+ # ["USPS Global Express Guaranteed Non-Document Non-Rectangular", 9400],
+ # ["USPS GXG Envelopes", 9400],
+ # ["USPS Global Express Guaranteed Non-Document Rectangular", 9400],
+ # ["USPS Global Express Guaranteed", 9400]]
+
+## TODO
+
+* proper documentation
+* proper offline testing for carriers in addition to the remote tests
+* package into a gem
+* carrier code template generator
+* more carriers
+* integrate with ActiveMerchant
+* support more features for existing carriers
+* bin-packing algorithm (preferably implemented in ruby)
+* order tracking
+* label printing
+
+## Contributing
+
+Yes, please! Take a look at the tests and the implementation of the Carrier class to see how the basics work. At some point soon there will be a carrier template generator along the lines of the gateway generator included in Active Merchant, but carrier.rb outlines most of what's necessary. The other main classes that would be good to familiarize yourself with are Location, Package, and Response.
+
+The nicest way to submit changes would be to set up a GitHub account and fork this project, then initiate a pull request when you want your changes looked at. You can also make a patch (preferably with [git-diff][]) and email to james@jadedpixel.com.
+
+[git-diff]:http://www.kernel.org/pub/software/scm/git/docs/git-diff.html
+
+## Contributors
+
+* Tobias Luetke (<http://blog.leetsoft.com>)
+* Cody Fauser (<http://codyfauser.com>)
+
+## Legal Mumbo Jumbo
+
+Unless otherwise noted in specific files, all code in the Active Shipping project is under the copyright and license described in the included MIT-LICENSE file.
49 Rakefile
@@ -0,0 +1,49 @@
+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'
+task :test => ['test:units','test:remote']
+
+# 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 = "ActiveShipping 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
1  init.rb
@@ -0,0 +1 @@
+require 'active_shipping'
44 lib/active_shipping.rb
@@ -0,0 +1,44 @@
+#--
+# Copyright (c) 2007 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__)
+
+
+
+require 'rubygems'
+require 'active_support'
+
+require 'vendor/xml_node/lib/xml_node'
+
+require 'net/https'
+require 'active_shipping/lib/requires_parameters'
+require 'active_shipping/lib/posts_data'
+require 'active_shipping/lib/country'
+
+require 'active_shipping/shipping/base'
+require 'active_shipping/shipping/response'
+require 'active_shipping/shipping/package'
+require 'active_shipping/shipping/location'
+require 'active_shipping/shipping/rate_estimate'
+require 'active_shipping/shipping/carrier'
+require 'active_shipping/shipping/carriers'
297 lib/active_shipping/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
83 lib/active_shipping/lib/posts_data.rb
@@ -0,0 +1,83 @@
+module ActiveMerchant #:nodoc:
+ class ActiveMerchantError < StandardError #:nodoc:
+ end
+
+ 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
17 lib/active_shipping/lib/requires_parameters.rb
@@ -0,0 +1,17 @@
+module ActiveMerchant
+ module RequiresParameters
+ def requires!(hash, *params)
+ keys = hash.keys
+ params.each do |param|
+ if param.is_a?(Array)
+ raise ArgumentError.new("Missing required parameter: #{param}") unless keys.include?(param.first)
+
+ valid_options = param[1..-1]
+ raise ArgumentError.new("Parameter :#{param.first} must be one of #{valid_options.inspect}") unless valid_options.include?(hash[param.first])
+ else
+ raise ArgumentError.new("Missing required parameter: #{param}") unless keys.include?(param)
+ end
+ end
+ end
+ end
+end
15 lib/active_shipping/shipping/base.rb
@@ -0,0 +1,15 @@
+module ActiveMerchant
+ module Shipping
+ module Base
+ mattr_accessor :mode
+ self.mode = :production
+
+ ALLCAPS_NAMES = ['ups','usps']
+
+ def self.carrier(name)
+ name = name.to_s.downcase
+ ActiveMerchant::Shipping.const_get(ALLCAPS_NAMES.include?(name) ? name.upcase : name.camelize)
+ end
+ end
+ end
+end
73 lib/active_shipping/shipping/carrier.rb
@@ -0,0 +1,73 @@
+module ActiveMerchant
+ module Shipping
+ class Carrier
+
+ include RequiresParameters
+ include PostsData
+
+ attr_reader :last_request
+ attr_accessor :test_mode
+ alias_method :test_mode?, :test_mode
+
+ # Override 'setup' method instead of 'initialize' in subclasses for most cases.
+ # Credentials should be in options hash under keys :login, :password and/or :key.
+ def initialize(options = {})
+ requirements.each {|key| requires!(options, key)}
+ @options = options
+ @last_request = nil
+ @test_mode = @options[:test]
+ setup
+ end
+
+ # Override to put any initializing code you want in this method; it gets called at the end of initialize.
+ def setup
+
+ end
+
+ # Override to return required keys in options hash for initialize method.
+ def requirements
+ []
+ end
+
+ # Override with whatever you need to get the rates
+ def find_rates(origin, destination, packages, options = {})
+ end
+
+ # Validate credentials with a call to the API. By default this just does a find_rates call
+ # with the orgin and destination both as the carrier's default_location. Override to provide
+ # alternate functionality, such as checking for test_mode to use test servers, etc.
+ def valid_credentials?
+ location = self.class.default_location
+ find_rates(location,location,Package.new(100, [5,15,30]))
+ rescue ActiveMerchant::Shipping::ResponseError
+ false
+ else
+ true
+ end
+
+ protected
+
+ # Override in subclasses for non-U.S.-based carriers.
+ def self.default_location
+ Location.new( :country => 'US',
+ :state => 'CA',
+ :city => 'Beverly Hills',
+ :address1 => '455 N. Rexford Dr.',
+ :address2 => '3rd Floor',
+ :zip => '90210',
+ :phone => '1-310-285-1013',
+ :fax => '1-310-275-8159')
+ end
+
+ # Use after building the request to save for later inspection. Probably won't ever be overridden.
+ def save_request(r)
+ @last_request = r
+ end
+
+ # Override in subclass to use for actual sending of request.
+ def commit(action, request, test = false)
+ end
+
+ end
+ end
+end
15 lib/active_shipping/shipping/carriers.rb
@@ -0,0 +1,15 @@
+require 'active_shipping/shipping/carriers/bogus_carrier'
+require 'active_shipping/shipping/carriers/ups'
+require 'active_shipping/shipping/carriers/usps'
+
+module ActiveMerchant
+ module Shipping
+ module Carriers
+ class <<self
+ def all
+ [BogusCarrier,UPS,USPS]
+ end
+ end
+ end
+ end
+end
16 lib/active_shipping/shipping/carriers/bogus_carrier.rb
@@ -0,0 +1,16 @@
+module ActiveMerchant
+ module Shipping
+ class BogusCarrier < Carrier
+ cattr_reader :name
+ @@name = "Bogus Carrier"
+
+
+ def find_rates(origin, destination, packages, options = {})
+ origin = Location.from(origin)
+ destination = Location.from(destination)
+ packages = Array(packages)
+ end
+
+ end
+ end
+end
260 lib/active_shipping/shipping/carriers/ups.rb
@@ -0,0 +1,260 @@
+module ActiveMerchant
+ module Shipping
+ class UPS < Carrier
+ cattr_accessor :default_options
+ cattr_reader :name
+ @@name = "UPS"
+
+ TEST_DOMAIN = 'wwwcie.ups.com'
+ LIVE_DOMAIN = 'www.ups.com'
+
+ RESOURCES = {
+ :rates => '/ups.app/xml/Rate'
+ }
+
+ USE_SSL = {
+ :rates => true
+ }
+
+ PICKUP_CODES = {
+ :daily_pickup => "01",
+ :customer_counter => "03",
+ :one_time_pickup => "06",
+ :on_call_air => "07",
+ :suggested_retail_rates => "11",
+ :letter_center => "19",
+ :air_service_center => "20"
+ }
+
+ DEFAULT_SERVICES = {
+ "01" => "UPS Next Day Air",
+ "02" => "UPS Second Day Air",
+ "03" => "UPS Ground",
+ "07" => "UPS Worldwide Express",
+ "08" => "UPS Worldwide Expedited",
+ "11" => "UPS Standard",
+ "12" => "UPS Three-Day Select",
+ "13" => "UPS Next Day Air Saver",
+ "14" => "UPS Next Day Air Early A.M.",
+ "54" => "UPS Worldwide Express Plus",
+ "59" => "UPS Second Day Air A.M.",
+ "65" => "UPS Saver",
+ "82" => "UPS Today Standard",
+ "83" => "UPS Today Dedicated Courier",
+ "84" => "UPS Today Intercity",
+ "85" => "UPS Today Express",
+ "86" => "UPS Today Express Saver"
+ }
+
+ CANADA_ORIGIN_SERVICES = {
+ "01" => "UPS Express",
+ "02" => "UPS Expedited",
+ "14" => "UPS Express Early A.M."
+ }
+
+ MEXICO_ORIGIN_SERVICES = {
+ "07" => "UPS Express",
+ "08" => "UPS Expedited",
+ "54" => "UPS Express Plus"
+ }
+
+ EU_ORIGIN_SERVICES = {
+ "07" => "UPS Express",
+ "08" => "UPS Expedited"
+ }
+
+ OTHER_NON_US_ORIGIN_SERVICES = {
+ "07" => "UPS Express"
+ }
+
+ # From http://en.wikipedia.org/w/index.php?title=European_Union&oldid=174718707 (Current as of November 30, 2007)
+ EU_COUNTRY_CODES = ["GB", "AT", "BE", "BG", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE"]
+
+ def requirements
+ [:key, :login, :password]
+ end
+
+ def find_rates(origin, destination, packages, options={})
+ options = @options.update(options)
+ packages = Array(packages)
+ access_request = build_access_request
+ rate_request = build_rate_request(origin, destination, packages, options)
+ response = commit(:rates, save_request(access_request + rate_request), (options[:test] || false))
+ parse_rate_response(origin, destination, packages, response, options)
+ end
+
+
+ protected
+ def build_access_request
+ xml_request = XmlNode.new('AccessRequest') do |access_request|
+ access_request << XmlNode.new('AccessLicenseNumber', @options[:key])
+ access_request << XmlNode.new('UserId', @options[:login])
+ access_request << XmlNode.new('Password', @options[:password])
+ end
+ xml_request.to_xml
+ end
+
+ def build_rate_request(origin, destination, packages, options={})
+ packages = Array(packages)
+ xml_request = XmlNode.new('RatingServiceSelectionRequest') do |root_node|
+ root_node << XmlNode.new('Request') do |request|
+ request << XmlNode.new('RequestAction', 'Rate')
+ request << XmlNode.new('RequestOption', 'Shop')
+ # not implemented: 'Rate' RequestOption to specify a single service query
+ # request << XmlNode.new('RequestOption', ((options[:service].nil? or options[:service] == :all) ? 'Shop' : 'Rate'))
+ end
+ root_node << XmlNode.new('PickupType') do |pickup_type|
+ pickup_type << XmlNode.new('Code', PICKUP_CODES[options[:pickup_type] || :daily_pickup])
+ # not implemented: PickupType/PickupDetails element
+ end
+ # not implemented: CustomerClassification element
+ root_node << XmlNode.new('Shipment') do |shipment|
+ # not implemented: Shipment/Description element
+ shipment << build_location_node('Shipper', (options[:shipper] || origin), options)
+ shipment << build_location_node('ShipTo', destination, options)
+ if options[:shipper] and options[:shipper] != origin
+ shipment << build_location_node('ShipFrom', origin, options)
+ end
+
+ # not implemented: * Shipment/ShipmentWeight element
+ # * Shipment/ReferenceNumber element
+ # * Shipment/Service element
+ # * Shipment/PickupDate element
+ # * Shipment/ScheduledDeliveryDate element
+ # * Shipment/ScheduledDeliveryTime element
+ # * Shipment/AlternateDeliveryTime element
+ # * Shipment/DocumentsOnly element
+
+ packages.each do |package|
+
+
+ imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))
+
+ shipment << XmlNode.new("Package") do |package_node|
+
+ # not implemented: * Shipment/Package/PackagingType element
+ # * Shipment/Package/Description element
+
+ package_node << XmlNode.new("PackagingType") do |packaging_type|
+ packaging_type << XmlNode.new("Code", '02')
+ end
+
+ package_node << XmlNode.new("Dimensions") do |dimensions|
+ dimensions << XmlNode.new("UnitOfMeasurement") do |units|
+ units << XmlNode.new("Code", imperial ? 'IN' : 'CM')
+ end
+ [:length,:width,:height].each do |axis|
+ value = ((imperial ? package.inches(axis) : package.cm(axis)).to_f*1000).round/1000.0 # 3 decimals
+ dimensions << XmlNode.new(axis.to_s.capitalize, [value,0.1].max)
+ end
+ end
+
+ package_node << XmlNode.new("PackageWeight") do |package_weight|
+ package_weight << XmlNode.new("UnitOfMeasurement") do |units|
+ units << XmlNode.new("Code", imperial ? 'LBS' : 'KGS')
+ end
+
+ value = ((imperial ? package.lbs : package.kgs).to_f*1000).round/1000.0 # 3 decimals
+ package_weight << XmlNode.new("Weight", [value,0.1].max)
+ end
+
+ # not implemented: * Shipment/Package/LargePackageIndicator element
+ # * Shipment/Package/ReferenceNumber element
+ # * Shipment/Package/PackageServiceOptions element
+ # * Shipment/Package/AdditionalHandling element
+ end
+
+ end
+
+ # not implemented: * Shipment/ShipmentServiceOptions element
+ # * Shipment/RateInformation element
+
+ end
+
+ end
+ xml_request.to_xml
+ end
+
+ def build_location_node(name,location,options={})
+ # not implemented: * Shipment/Shipper/Name element
+ # * Shipment/(ShipTo|ShipFrom)/CompanyName element
+ # * Shipment/(Shipper|ShipTo|ShipFrom)/AttentionName element
+ # * Shipment/(Shipper|ShipTo|ShipFrom)/TaxIdentificationNumber element
+ location_node = XmlNode.new(name) do |location_node|
+ location_node << XmlNode.new('PhoneNumber', location.phone.gsub(/[^\d]/,'')) unless location.phone.blank?
+ location_node << XmlNode.new('FaxNumber', location.fax.gsub(/[^\d]/,'')) unless location.fax.blank?
+
+ if name == 'Shipper' and (origin_account = @options[:origin_account] || options[:origin_account])
+ location_node << XmlNode.new('ShipperNumber', origin_account)
+ elsif name == 'ShipTo' and (destination_account = @options[:destination_account] || options[:destination_account])
+ location_node << XmlNode.new('ShipperAssignedIdentificationNumber', destination_account)
+ end
+
+ location_node << XmlNode.new('Address') do |address|
+ address << XmlNode.new("AddressLine1", location.address1) unless location.address1.blank?
+ address << XmlNode.new("AddressLine2", location.address2) unless location.address2.blank?
+ address << XmlNode.new("AddressLine3", location.address3) unless location.address3.blank?
+ address << XmlNode.new("City", location.city) unless location.city.blank?
+ address << XmlNode.new("StateProvinceCode", location.province) unless location.province.blank?
+ # StateProvinceCode required for negotiated rates but not otherwise, for some reason
+ address << XmlNode.new("PostalCode", location.postal_code) unless location.postal_code.blank?
+ address << XmlNode.new("CountryCode", location.country_code(:alpha2)) unless location.country_code(:alpha2).blank?
+ # not implemented: Shipment/(Shipper|ShipTo|ShipFrom)/Address/ResidentialAddressIndicator element
+ end
+ end
+ end
+
+ def parse_rate_response(origin, destination, packages, response, options={})
+ rates = []
+
+ xml_hash = Hash.from_xml(response)['RatingServiceSelectionResponse']
+ success = xml_hash['Response']['ResponseStatusCode'] == '1'
+
+ if success
+ rate_estimates = []
+ message = xml_hash['Response']['ResponseStatusDescription']
+
+ xml_hash['RatedShipment'] = [xml_hash['RatedShipment']] unless xml_hash['RatedShipment'].is_a? Array
+ xml_hash['RatedShipment'].each do |rated_shipment|
+ service_code = rated_shipment['Service']['Code']
+ rate_estimates << RateEstimate.new(origin, destination, @@name,
+ service_name_for(origin, service_code),
+ :total_price => rated_shipment['TotalCharges']['MonetaryValue'].to_f,
+ :currency => rated_shipment['TotalCharges']['CurrencyCode'],
+ :service_code => service_code,
+ :packages => packages)
+ end
+ else
+ message = xml_hash['Response']['Error']['ErrorDescription']
+ end
+ Response.new(success, message, xml_hash, :rates => rate_estimates, :xml => response, :request => last_request)
+ end
+
+ def commit(action, request, test = false)
+ http = Net::HTTP.new((test ? TEST_DOMAIN : LIVE_DOMAIN),
+ (USE_SSL[action] ? 443 : 80 ))
+ http.use_ssl = USE_SSL[action]
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if USE_SSL[action]
+ response = http.start do |http|
+ http.post RESOURCES[action], request
+ end
+ response.body
+ end
+
+
+ def service_name_for(origin, code)
+ origin = origin.country_code(:alpha2)
+
+ name = case origin
+ when "CA": CANADA_ORIGIN_SERVICES[code]
+ when "MX": MEXICO_ORIGIN_SERVICES[code]
+ when *EU_COUNTRY_CODES: EU_ORIGIN_SERVICES[code]
+ end
+
+ name ||= OTHER_NON_US_ORIGIN_SERVICES[code] unless name == 'US'
+ name ||= DEFAULT_SERVICES[code]
+ end
+
+ end
+ end
+end
375 lib/active_shipping/shipping/carriers/usps.rb
@@ -0,0 +1,375 @@
+module ActiveMerchant
+ module Shipping
+ class USPS < Carrier
+ include ActiveMerchant::Shipping
+ cattr_reader :name
+ @@name = "USPS"
+
+ LIVE_DOMAIN = 'production.shippingapis.com'
+ LIVE_RESOURCE = '/ShippingAPI.dll'
+
+ TEST_DOMAINS = { #indexed by security; e.g. TEST_DOMAINS[USE_SSL[:rates]]
+ true => 'secure.shippingapis.com',
+ false => 'testing.shippingapis.com'
+ }
+
+ TEST_RESOURCE = '/ShippingAPITest.dll'
+
+ API_CODES = {
+ :us_rates => 'RateV3',
+ :world_rates => 'IntlRate',
+ :test => 'CarrierPickupAvailability'
+ }
+ USE_SSL = {
+ :us_rates => false,
+ :world_rates => false,
+ :test => true
+ }
+ CONTAINERS = {
+ :envelope => 'Flat Rate Envelope',
+ :box => 'Flat Rate Box'
+ }
+ MAIL_TYPES = {
+ :package => 'Package',
+ :postcard => 'Postcards or aerogrammes',
+ :matter_for_the_blind => 'Matter for the blind',
+ :envelope => 'Envelope'
+ }
+ PACKAGE_PROPERTIES = {
+ 'ZipOrigination' => :origin_zip,
+ 'ZipDestination' => :destination_zip,
+ 'Pounds' => :pounds,
+ 'Ounces' => :ounces,
+ 'Container' => :container,
+ 'Size' => :size,
+ 'Machinable' => :machinable,
+ 'Zone' => :zone,
+ 'Postage' => :postage,
+ 'Restrictions' => :restrictions
+ }
+ POSTAGE_PROPERTIES = {
+ 'MailService' => :service,
+ 'Rate' => :rate
+ }
+ US_SERVICES = {
+ :first_class => 'FIRST CLASS',
+ :priority => 'PRIORITY',
+ :express => 'EXPRESS',
+ :bpm => 'BPM',
+ :parcel => 'PARCEL',
+ :media => 'MEDIA',
+ :library => 'LIBRARY',
+ :all => 'ALL'
+ }
+
+ # Do these as we find them, I guess...
+ COUNTRY_NAME_CONVERSIONS = {
+ 'United Kingdom' => 'Great Britain'
+ }
+
+ class <<self
+ def size_code_for(package)
+ total = package.inches(:length) + package.inches(:girth)
+ if total <= 84
+ return 'REGULAR'
+ elsif total <= 108
+ return 'LARGE'
+ else # <= 130
+ return 'OVERSIZE'
+ end
+ end
+ end
+
+ def requirements
+ [:login]
+ end
+
+ def find_rates(origin, destination, packages, options = {})
+ options = @options.update(options)
+
+ origin = Location.from(origin)
+ destination = Location.from(destination)
+ packages = Array(packages)
+
+ #raise ArgumentError.new("USPS packages must originate in the U.S.") unless ['US',nil].include?(origin.country_code(:alpha2))
+
+
+ # domestic or international?
+
+ response = if ['US',nil].include?(destination.country_code(:alpha2))
+ us_rates(origin, destination, packages, options)
+ else
+ world_rates(origin, destination, packages, options)
+ end
+ end
+
+ def valid_credentials?
+ # Cannot test with find_rates because the airheads at USPS don't allow that in test mode
+ test_mode? ? canned_address_verification_works? : super
+ end
+
+ protected
+
+ def us_rates(origin, destination, packages, options={})
+ request = build_us_rate_request(packages, origin.zip, destination.zip)
+ # never use test mode; rate requests just won't work on test servers
+ parse_response origin, destination, packages, commit(:us_rates,request,false)
+ end
+
+ def world_rates(origin, destination, packages, options={})
+ request = build_world_rate_request(packages, destination.country)
+ # never use test mode; rate requests just won't work on test servers
+ parse_response origin, destination, packages, commit(:world_rates,request,false)
+ end
+
+ # Once the address verification API is implemented, remove this and have valid_credentials? build the request using that instead.
+ def canned_address_verification_works?
+ request = "%3CCarrierPickupAvailabilityRequest%20USERID=%22#{@options[:login]}%22%3E%20%0A%3CFirmName%3EABC%20Corp.%3C/FirmName%3E%20%0A%3CSuiteOrApt%3ESuite%20777%3C/SuiteOrApt%3E%20%0A%3CAddress2%3E1390%20Market%20Street%3C/Address2%3E%20%0A%3CUrbanization%3E%3C/Urbanization%3E%20%0A%3CCity%3EHouston%3C/City%3E%20%0A%3CState%3ETX%3C/State%3E%20%0A%3CZIP5%3E77058%3C/ZIP5%3E%20%0A%3CZIP4%3E1234%3C/ZIP4%3E%20%0A%3C/CarrierPickupAvailabilityRequest%3E%0A"
+ expected_hash = {"CarrierPickupAvailabilityResponse"=>{"City"=>"HOUSTON", "Address2"=>"1390 Market Street", "FirmName"=>"ABC Corp.", "State"=>"TX", "Date"=>"3/1/2004", "DayOfWeek"=>"Monday", "Urbanization"=>nil, "ZIP4"=>"1234", "ZIP5"=>"77058", "CarrierRoute"=>"C", "SuiteOrApt"=>"Suite 777"}}
+ xml = commit(:test, request, true)
+ response_hash = Hash.from_xml(xml)
+ response_hash == expected_hash
+ end
+
+ def build_us_rate_request(packages, origin_zip, destination_zip, options={})
+ request = XmlNode.new('RateV3Request', :USERID => @options[:login]) do |rate_request|
+ packages.each_index do |id|
+ p = packages[id]
+ rate_request << XmlNode.new('Package', :ID => id.to_s) do |package|
+ package << XmlNode.new('Service', US_SERVICES[options[:service] || :all])
+ package << XmlNode.new('ZipOrigination', origin_zip)
+ package << XmlNode.new('ZipDestination', destination_zip)
+ package << XmlNode.new('Pounds', 0)
+ package << XmlNode.new('Ounces', "%0.1f" % [p.ounces,1].max)
+ if p.options[:container] and [nil,:all,:express,:priority].include? p.service
+ package << XmlNode.new('Container', CONTAINERS[p.options[:container]])
+ end
+ package << XmlNode.new('Size', USPS.size_code_for(p))
+ package << XmlNode.new('Width', p.inches(:width))
+ package << XmlNode.new('Length', p.inches(:length))
+ package << XmlNode.new('Height', p.inches(:height))
+ package << XmlNode.new('Girth', p.inches(:girth))
+ package << XmlNode.new('Machinable', (p.options[:machinable] ? true : false).to_s.upcase)
+ end
+ end
+ end
+ URI.encode(save_request(request.to_s))
+ end
+
+ # important difference with international rate requests:
+ # * services are not given in the request
+ # * package sizes are not given in the request
+ # * services are returned in the response along with restrictions of size
+ # * the size restrictions are returned AS AN ENGLISH SENTENCE (!?)
+
+ def build_world_rate_request(packages, destination_country)
+ country = COUNTRY_NAME_CONVERSIONS[destination_country.name] || destination_country.name
+ request = XmlNode.new('IntlRateRequest', :USERID => @options[:login]) do |rate_request|
+ packages.each_index do |id|
+ p = packages[id]
+ rate_request << XmlNode.new('Package', :ID => id.to_s) do |package|
+ package << XmlNode.new('Pounds', 0)
+ package << XmlNode.new('Ounces', [p.ounces,1].max.ceil) #takes an integer for some reason, must be rounded UP
+ package << XmlNode.new('MailType', MAIL_TYPES[p.options[:mail_type]] || 'Package')
+ package << XmlNode.new('ValueOfContents', p.value / 100.0) if p.value && p.currency == 'USD'
+ package << XmlNode.new('Country', country)
+ end
+ end
+ end
+ URI.encode(save_request(request.to_s))
+ end
+
+ def parse_response(origin, destination, packages, response, options={})
+ success = true
+ message = ''
+ rate_hash = {}
+ response_hash = Hash.from_xml(response)
+ root_node_name = response_hash.keys.first
+ root_node = response_hash[root_node_name]
+
+ root_node['Package'] = [root_node['Package']] unless root_node['Package'].is_a? Array
+
+ if root_node_name == 'Error'
+ success = false
+ message = root_node['Description']
+ else
+ root_node['Package'].each do |package|
+ if package['Error']
+ success = false
+ message = package['Error']['Description']
+ break
+ end
+ end
+
+ if success
+ rate_hash = rates_from_response_hash(response_hash, packages)
+ unless rate_hash
+ success = false
+ message = "Unknown root node in XML response: '#{root_node_name}'"
+ end
+ end
+
+ end
+
+
+
+ rate_estimates = rate_hash.keys.map do |service_name|
+ RateEstimate.new(origin,destination,@@name,"USPS #{service_name}",
+ :package_rates => rate_hash[service_name][:package_rates],
+ :service_code => rate_hash[service_name][:service_code],
+ :currency => 'USD')
+ end
+ rate_estimates.reject! {|e| e.package_count != packages.length}
+ rate_estimates = rate_estimates.sort_by(&:total_price)
+
+ Response.new(success, message, response_hash, :rates => rate_estimates, :xml => response, :request => last_request)
+ end
+
+ def rates_from_response_hash(response_hash, packages)
+ rate_hash = {}
+ root_node = response_hash.keys.find {|key| ['IntlRateResponse','RateV3Response'].include? key }
+ return false unless root_node
+ domestic = (root_node == 'RateV3Response')
+ root_node = response_hash[root_node]
+
+ if domestic
+ service_node, service_code_node, service_name_node, rate_node = 'Postage', 'CLASSID', 'MailService', 'Rate'
+ else
+ service_node, service_code_node, service_name_node, rate_node = 'Service', 'ID', 'SvcDescription', 'Postage'
+ end
+
+ root_node['Package'] = [root_node['Package']] unless root_node['Package'].is_a? Array
+
+
+ root_node['Package'].each do |package_hash|
+ package_index = package_hash['ID'].to_i
+
+ package_hash[service_node] = [package_hash[service_node]] unless package_hash[service_node].is_a? Array
+
+ package_hash[service_node].each do |service_response_hash|
+ service_name = service_response_hash[service_name_node]
+
+ # aggregate specific package rates into a service-centric RateEstimate
+ # first package with a given service name will initialize these;
+ # later packages with same service will add to them
+ this_service = rate_hash[service_name] ||= {}
+ this_service[:service_code] ||= service_response_hash[service_code_node]
+ package_rates = this_service[:package_rates] ||= []
+
+
+ this_package_rate = {:package => (this_package = packages[package_index]),
+ :rate => Package.cents_from(service_response_hash[rate_node].to_f)}
+
+ # unless this_service[:options] || domestic
+ # commitments = service_response_hash['SvcCommitments'].split(/[^\d]/).reject {|str| str.empty?}.map {|c| c.to_i}
+ # estimated_days = (commitments.empty? ? nil : ((commitments.first)..(commitments.last)))
+ # this_service[:options] ||= {:estimated_days => estimated_days,
+ # :max_dimensions => service_response_hash['MaxDimensions']}
+ # end
+
+ package_rates << this_package_rate if package_valid_for_service(this_package,service_response_hash)
+
+ if package_valid_for_service(this_package,service_response_hash)
+ else
+ end
+ end
+ # sorted = package_hash['Service'].sort {|x,y| #sort by rate descending
+ # y['Postage'].to_f <=> x['Postage'].to_f}
+ # filtered = sorted.map do |s| #map to hashes of the data we want
+ # if package_valid_for_service(packages[i],s)
+ # commitments = s['SvcCommitments'].split(/[^\d]/).reject {|str| str.empty?}.map {|c| c.to_i}
+ # { :name => s['SvcDescription'],
+ # :rate => (s['Postage'].to_f * 100).to_i,
+ # :estimated_days => (commitments.empty? ? nil : ((commitments.first)..(commitments.last))),
+ # :max_dimensions => s['MaxDimensions'] }
+ # else
+ # nil
+ # end
+ # end
+ # rates << filtered.compact
+ end
+ rate_hash
+ end
+
+ def package_valid_for_service(package,service_hash)
+ return true if service_hash['MaxWeight'].nil?
+ max_weight = service_hash['MaxWeight'].to_f
+ name = (service_hash['SvcDescription'] || service_hash['MailService']).downcase
+ if name =~ /flat.rate.box/ #domestic or international flat rate box
+
+ # flat rate dimensions from http://www.usps.com/shipping/flatrate.htm
+ return (package_valid_for_max_dimensions(package,
+ :weight => max_weight, #domestic apparently has no weight restriction
+ :length => 11.0,
+ :width => 8.5,
+ :height => 5.5) or
+ package_valid_for_max_dimensions(package,
+ :weight => max_weight,
+ :length => 13.625,
+ :width => 11.875,
+ :height => 3.375))
+ elsif name =~ /flat.rate.envelope/
+ return package_valid_for_max_dimensions(package,
+ :weight => max_weight,
+ :length => 12.5,
+ :width => 9.5,
+ :height => 0.75)
+ elsif service_hash['MailService'] # domestic non-flat rates
+ return true
+ else #international non-flat rates
+
+ # Some sample english that this is required to parse:
+ #
+ # 'Max. length 46", width 35", height 46" and max. length plus girth 108"'
+ # 'Max. length 24", Max. length, height, depth combined 36"'
+ #
+ tokens = service_hash['MaxDimensions'].downcase.split(/[^\d]*"/).reject {|t| t.empty?}
+ max_dimensions = {:weight => max_weight}
+ single_axis_values = []
+ tokens.each do |token|
+ axis_sum = [/length/,/width/,/height/,/depth/].sum {|regex| (token =~ regex) ? 1 : 0}
+ unless axis_sum == 0
+ value = token[(token =~ /\d+$/)..-1].to_f
+ if axis_sum == 3
+ max_dimensions[:length_plus_width_plus_height] = value
+ elsif token =~ /girth/ and axis_sum == 1
+ max_dimensions[:length_plus_girth] = value
+ else
+ single_axis_values << value
+ end
+ end
+ end
+ single_axis_values.sort!.reverse!
+ [:length, :width, :height].each_with_index do |axis,i|
+ max_dimensions[axis] = single_axis_values[i] if single_axis_values[i]
+ end
+ return package_valid_for_max_dimensions(package, max_dimensions)
+ end
+ end
+
+ def package_valid_for_max_dimensions(package,dimensions)
+ valid = ((not ([:length,:width,:height].map {|dim| dimensions[dim].nil? || dimensions[dim].to_f >= package.inches(dim).to_f}.include?(false))) and
+ (dimensions[:weight].nil? || dimensions[:weight] >= package.pounds) and
+ (dimensions[:length_plus_girth].nil? or
+ dimensions[:length_plus_girth].to_f >=
+ package.inches(:length) + package.inches(:girth)) and
+ (dimensions[:length_plus_width_plus_height].nil? or
+ dimensions[:length_plus_width_plus_height].to_f >=
+ package.inches(:length) + package.inches(:width) + package.inches(:height)))
+
+ return valid
+ end
+
+ def commit(action, request, test = false)
+ http = Net::HTTP.new((test ? TEST_DOMAINS[USE_SSL[action]] : LIVE_DOMAIN),
+ (USE_SSL[action] ? 443 : 80 ))
+ http.use_ssl = USE_SSL[action]
+ response = http.start do |http|
+ http.get "#{test ? TEST_RESOURCE : LIVE_RESOURCE}?API=#{API_CODES[action]}&XML=#{request}"
+ end
+ response.body
+ end
+
+ end
+ end
+end
92 lib/active_shipping/shipping/location.rb
@@ -0,0 +1,92 @@
+module ActiveMerchant #:nodoc:
+ module Shipping #:nodoc:
+ class Location
+
+ attr_reader :options,
+ :country,
+ :postal_code,
+ :province,
+ :city,
+ :address1,
+ :address2,
+ :address3,
+ :phone,
+ :fax
+
+ alias_method :zip, :postal_code
+ alias_method :postal, :postal_code
+ alias_method :state, :province
+ alias_method :territory, :province
+ alias_method :region, :province
+
+ def initialize(options = {})
+ @country = (options[:country].nil? or options[:country].is_a?(ActiveMerchant::Country)) ?
+ options[:country] :
+ ActiveMerchant::Country.find(options[:country])
+ @postal_code = options[:postal_code] || options[:postal] || options[:zip]
+ @province = options[:province] || options[:state] || options[:territory] || options[:region]
+ @city = options[:city]
+ @address1 = options[:address1]
+ @address2 = options[:address2]
+ @address3 = options[:address3]
+ @phone = options[:phone]
+ @fax = options[:fax]
+ end
+
+ def self.from(object, options={})
+ return object if object.is_a? ActiveMerchant::Shipping::Location
+ attr_mappings = {
+ :country => [:country_code, :country],
+ :postal_code => [:postal_code, :zip, :postal],
+ :province => [:province_code, :state_code, :territory_code, :region_code, :province, :state, :territory, :region],
+ :city => [:city, :town],
+ :address1 => [:address1, :address, :street],
+ :address2 => [:address2],
+ :address3 => [:address3],
+ :phone => [:phone, :phone_number],
+ :fax => [:fax, :fax_number]
+ }
+ attributes = {}
+ hash_access = begin
+ object[:some_symbol]
+ true
+ rescue
+ false
+ end
+ attr_mappings.each do |pair|
+ pair[1].each do |sym|
+ if value = (object[sym] if hash_access) || (object.send(sym) if object.respond_to?(sym) && (!hash_access || !Hash.public_instance_methods.include?(sym.to_s)))
+ attributes[pair[0]] = value
+ break
+ end
+ end
+ end
+ self.new(attributes.update(options))
+ end
+
+ def country_code(format)
+ @country.nil? ? nil : @country.code(format).first.value
+ end
+
+ def to_s
+ prettyprint.gsub(/\n/, ' ')
+ end
+
+ def prettyprint
+ chunks = []
+ chunks << [@address1,@address2,@address3].reject {|e| e.blank?}.join("\n")
+ chunks << [@city,@province,@postal_code].reject {|e| e.blank?}.join(', ')
+ chunks << @country
+ chunks.reject {|e| e.blank?}.join("\n")
+ end
+
+ def inspect
+ string = prettyprint
+ string << "\nPhone: #{@phone}" unless @phone.blank?
+ string << "\nFax: #{@fax}" unless @fax.blank?
+ string
+ end
+ end
+
+ end
+end
134 lib/active_shipping/shipping/package.rb
@@ -0,0 +1,134 @@
+module ActiveMerchant #:nodoc:
+ module Shipping #:nodoc:
+ class Package
+ GRAMS_IN_AN_OUNCE = 28.3495231
+ OUNCES_IN_A_GRAM = 0.0352739619
+ INCHES_IN_A_CM = 0.393700787
+ CM_IN_AN_INCH = 2.54
+
+ cattr_accessor :default_options
+ attr_reader :options, :value, :currency
+
+ # Package.new(100, [10, 20, 30], :units => :metric)
+ def initialize(grams_or_ounces, dimensions, options = {})
+ options = @@default_options.update(options) if @@default_options
+ options.symbolize_keys!
+ @options = options
+
+ imperial = options[:units] == :imperial
+ dimensions = Array(dimensions)
+
+ @ounces,@grams = nil
+ if grams_or_ounces.nil?
+ @grams = @ounces = 0
+ elsif imperial
+ @ounces = grams_or_ounces
+ else
+ @grams = grams_or_ounces
+ end
+
+ @inches,@centimetres = nil
+ if dimensions.empty?
+ @inches = @centimetres = [0,0,0]
+ else
+ process_dimensions(dimensions,imperial)
+ end
+
+ @value = Package.cents_from(options[:value])
+ @currency = options[:currency] || (options[:value].currency if options[:value].respond_to?(:currency))
+ @cylinder = (options[:cylinder] || options[:tube]) ? true : false
+ end
+
+ def cylinder?
+ @cylinder
+ end
+ alias_method :tube?, :cylinder?
+
+ def ounces(options={})
+ case options[:type]
+ when *[nil,:actual]: @ounces ||= grams(options) * OUNCES_IN_A_GRAM
+ when *[:volumetric,:dimensional]: @volumetric_ounces ||= grams(options) * OUNCES_IN_A_GRAM
+ when :billable: @billable_ounces ||= [ounces,ounces(:type => :volumetric)].max
+ end
+ end
+ alias_method :oz, :ounces
+
+ def grams(options={})
+ case options[:type]
+ when *[nil,:actual]: @grams ||= ounces(options) * GRAMS_IN_AN_OUNCE
+ when *[:volumetric,:dimensional]: @volumetric_grams ||= centimetres(:box_volume) / 6.0
+ when :billable: [grams,grams(:type => :volumetric)].max
+ end
+ end
+ alias_method :g, :grams
+
+ def pounds(options={})
+ ounces(options) / 16.0
+ end
+ alias_method :lb, :pounds
+ alias_method :lbs, :pounds
+
+ def kilograms(options={})
+ grams(options) / 1000.0
+ end
+ alias_method :kg, :kilograms
+ alias_method :kgs, :kilograms
+
+ def inches(measurement=nil)
+ @inches ||= @centimetres.map {|cm| cm * INCHES_IN_A_CM}
+ measurement.nil? ? @inches : measure(measurement, @inches)
+ end
+ alias_method :in, :inches
+
+ def centimetres(measurement=nil)
+ @centimetres ||= @inches.map {|inches| inches * CM_IN_AN_INCH}
+ measurement.nil? ? @centimetres : measure(measurement, @centimetres)
+ end
+ alias_method :cm, :centimetres
+
+ def self.cents_from(money)
+ return nil if money.nil?
+ if money.respond_to?(:cents)
+ return money.cents
+ else
+ return case money
+ when Float
+ (money * 100).to_i
+ when String
+ money =~ /\./ ? (money.to_f * 100).to_i : money.to_i
+ else
+ money.to_i
+ end
+ end
+ end
+
+ private
+
+ def measure(measurement, ary)
+ case measurement
+ when Fixnum: ary[measurement]
+ when *[:x,:max,:length,:long]: ary[2]
+ when *[:y,:mid,:width,:wide]: ary[1]
+ when *[:z,:min,:height,:depth,:high,:deep]: ary[0]
+ when *[:girth,:around,:circumference]
+ self.cylinder? ? (Math::PI * (ary[0] + ary[1]) / 2) : (2 * ary[0]) + (2 * ary[1])
+ when :volume: self.cylinder? ? (Math::PI * (ary[0] + ary[1]) / 4)**2 * ary[2] : measure(:box_volume,ary)
+ when :box_volume: ary[0] * ary[1] * ary[2]
+ end
+ end
+
+ def process_dimensions(dimensions, imperial_units)
+ units = imperial_units ? 'inches' : 'centimetres'
+ self.instance_variable_set("@#{units}", dimensions.sort)
+ units_array = self.instance_variable_get("@#{units}")
+ # [1,2] => [1,1,2]
+ # [5] => [5,5,5]
+ # etc..
+ 2.downto(units_array.length) do |n|
+ units_array.unshift(units_array[0])
+ end
+ end
+
+ end
+ end
+end
52 lib/active_shipping/shipping/rate_estimate.rb
@@ -0,0 +1,52 @@
+module ActiveMerchant #:nodoc:
+ module Shipping #:nodoc:
+
+ class RateEstimate
+ attr_reader :origin # Location objects
+ attr_reader :destination
+ attr_reader :package_rates # array of hashes in the form of {:package => <Package>, :rate => 500}
+ attr_reader :carrier # Carrier.name ('USPS', 'FedEx', etc.)
+ attr_reader :service_name # name of service ("First Class Ground", etc.)
+ attr_reader :service_code
+ attr_reader :currency # 'USD', 'CAD', etc.
+ # http://en.wikipedia.org/wiki/ISO_4217
+
+ def initialize(origin, destination, carrier, service_name, options={})
+ @origin, @destination, @carrier, @service_name = origin, destination, carrier, service_name
+ @service_code = options[:service_code]
+ if options[:package_rates]
+ @package_rates = options[:package_rates].map {|p| p.update({:rate => Package.cents_from(p[:rate])}) }
+ else
+ @package_rates = Array(options[:packages]).map {|p| {:package => p}}
+ end
+ @total_price = Package.cents_from(options[:total_price])
+ @currency = options[:currency]
+ end
+
+ def total_price
+ begin
+ @total_price || @package_rates.sum {|p| p[:rate]}
+ rescue NoMethodError
+ raise ArgumentError.new("RateEstimate must have a total_price set, or have a full set of valid package rates.")
+ end
+ end
+ alias_method :price, :total_price
+
+ def add(package,rate=nil)
+ cents = Package.cents_from(rate)
+ raise ArgumentError.new("New packages must have valid rate information since this RateEstimate has no total_price set.") if cents.nil? and total_price.nil?
+ @package_rates << {:package => package, :rate => cents}
+ self
+ end
+
+ def packages
+ package_rates.map {|p| p[:package]}
+ end
+
+ def package_count
+ package_rates.length
+ end
+
+ end
+ end
+end
55 lib/active_shipping/shipping/response.rb
@@ -0,0 +1,55 @@
+module ActiveMerchant #:nodoc:
+
+ class ActiveMerchantError < StandardError #:nodoc:
+ end
+
+ module Shipping #:nodoc:
+
+ class Error < ActiveMerchant::ActiveMerchantError
+ end
+
+ class ResponseError < Error
+ attr_reader :response
+
+ def initialize(response = nil)
+ if response.is_a? Response
+ super(response.message)
+ @response = response
+ else
+ super(response)
+ end
+ end
+ end
+
+ class Response
+
+ attr_reader :params
+ attr_reader :message
+ attr_reader :test
+ attr_reader :rates
+ attr_reader :xml
+ attr_reader :request
+
+ def initialize(success, message, params = {}, options = {})
+ @success, @message, @params = success, message, params.stringify_keys
+ @test = options[:test] || false
+ @rates = Array(options[:estimates] || options[:rates] || options[:rate_estimates])
+ @xml = options[:xml]
+ @request = options[:request]
+ raise ResponseError.new(self) unless success
+ end
+
+ def success?
+ @success ? true : false
+ end
+
+ def test?
+ @test ? true : false
+ end
+
+ alias_method :estimates, :rates
+ alias_method :rate_estimates, :rates
+
+ end
+ end
+end
7,815 lib/certs/cacert.pem
7,815 additions, 0 deletions not shown
13 lib/vendor/test_helper.rb
@@ -0,0 +1,13 @@
+#!/usr/bin/env ruby
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+
+
+require 'test/unit'
+require 'active_shipping'
+
+begin
+ require 'mocha'
+rescue LoadError
+ require 'rubygems'
+ require 'mocha'
+end
36 lib/vendor/xml_node/README
@@ -0,0 +1,36 @@
+XML Node
+==========
+
+
+Example for generating xml:
+
+ # Create an atom like document
+
+ root = XmlNode.new 'feed' do |feed|
+
+ feed << XmlNode.new('id', 'tag:atom.com,2007:1')
+ feed << XmlNode.new('title', 'Atom test feed')
+ feed << XmlNode.new('author') do |author|
+ author << XmlNode.new("name", "tobi")
+ author << XmlNode.new("email", "tobi@gmail.com")
+ end
+
+ feed << XmlNode.new('entry') do |entry|
+ entry << XmlNode.new('title', 'First post')
+ entry << XmlNode.new('summary', 'Lorem ipsum', :type => 'xhtml')
+ entry << XmlNode.new('created_at', Time.now)
+ end
+
+ feed << XmlNode.new('dc:published', Time.now)
+ end
+
+ root.to_xml #=> Well formatted xml
+
+
+Example for parsing xml:
+
+
+ xml = XmlNode.parse('<feed attr="1"><element>text</element><element>text</element></feed>')
+ xml['attr'] #=> '1'
+ xml.children['element'].text #=> 'text'
+ xml.children.each { |e| e... }
21 lib/vendor/xml_node/Rakefile
@@ -0,0 +1,21 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/test_*.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the calculations plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'XmlNode'
+ rdoc.options << '--line-numbers --inline-source'
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
32 lib/vendor/xml_node/benchmark/bench_generation.rb
@@ -0,0 +1,32 @@
+require "benchmark"
+require File.dirname(__FILE__) + "/../lib/xml_node"
+
+class XmlNode
+
+ def to_xml_as_array
+ xml = []
+ document = REXML::Document.new
+ document << REXML::XMLDecl.new('1.0')
+ document << @element
+ document.write( xml, 0)
+ xml.to_s
+ end
+
+ def to_xml_no_format
+ xml = ''
+ document = REXML::Document.new
+ document << REXML::XMLDecl.new('1.0')
+ document << @element
+ document.write( xml)
+ xml
+ end
+
+end
+
+TESTS = 10000
+
+Benchmark.bmbm do |results|
+ results.report { TESTS.times do XmlNode.new('feed') { |n| n << XmlNode.new('element', 'test'); n << XmlNode.new('element') }.to_xml end }
+ results.report { TESTS.times do XmlNode.new('feed') { |n| n << XmlNode.new('element', 'test'); n << XmlNode.new('element') }.to_xml_as_array end }
+ results.report { TESTS.times do XmlNode.new('feed') { |n| n << XmlNode.new('element', 'test'); n << XmlNode.new('element') }.to_xml_no_format end }
+end
1  lib/vendor/xml_node/init.rb
@@ -0,0 +1 @@
+# Include hook code here
220 lib/vendor/xml_node/lib/xml_node.rb
@@ -0,0 +1,220 @@
+require 'rexml/document'
+
+class Object
+ def to_xml_value
+ to_s
+ end
+end
+
+class NilClass
+ def to_xml_value
+ nil
+ end
+end
+
+class TrueClass
+ def to_xml_value
+ to_s
+ end
+end
+
+class FalseClass
+ def to_xml_value
+ to_s
+ end
+end
+
+class Time
+ def to_xml_value
+ self.xmlschema
+ end
+end
+
+class DateTime
+ def to_xml_value
+ self.xmlschema
+ end
+end
+
+class Date
+ def to_xml_value
+ self.to_time.xmlschema
+ end
+end
+
+class REXML::Element
+ def to_xml_element
+ self
+ end
+end
+
+class XmlNode
+ attr_accessor :child_nodes
+ attr_reader :element
+
+ class List
+ include Enumerable
+
+ def initialize(parent)
+ @parent = parent
+ @children = {}
+ end
+
+ def [](value)
+ node_for @parent.element.elements[value]
+ end
+
+ def []=(value, key)
+ @parent.element.elements[value.to_s] = key.to_xml_element
+ end
+
+ def each(&block)
+ @parent.element.each_element { |e| yield node_for(e) }
+ end
+
+ private
+
+ def node_for(element)
+ @parent.child_nodes[element] ||= XmlNode.new(element)
+ end
+ end
+
+ # Allows for very pretty xml generation akin to xml builder.
+ #
+ # Example:
+ #
+ # # Create an atom like document
+ # doc = Document.new
+ # doc.root = XmlNode.new 'feed' do |feed|
+ #
+ # feed << XmlNode.new('id', 'tag:atom.com,2007:1')
+ # feed << XmlNode.new('title', 'Atom test feed')
+ # feed << XmlNode.new('author') do |author|
+ # author << XmlNode.new("name", "tobi")
+ # author << XmlNode.new("email", "tobi@gmail.com")
+ # end
+ #
+ # feed << XmlNode.new('entry') do |entry|
+ # entry << XmlNode.new('title', 'First post')
+ # entry << XmlNode.new('summary', 'Lorem ipsum', :type => 'xhtml')
+ # entry << XmlNode.new('created_at', Time.now)
+ # end
+ #
+ # feed << XmlNode.new('dc:published', Time.now)
+ # end
+ #
+ def initialize(node, *args)
+ @element = if node.is_a?(REXML::Element)
+ node
+ else
+ REXML::Element.new(node)
+ end
+
+ @child_nodes = {}
+
+ if attributes = args.last.is_a?(Hash) ? args.pop : nil
+ attributes.each { |k,v| @element.add_attribute(k.to_s, v.to_xml_value) }
+ end
+
+ if !args[0].nil?
+ @element.text = args[0].to_xml_value
+ end
+
+ if block_given?
+ yield self
+ end
+ end
+
+ def self.parse(xml)
+ self.new(REXML::Document.new(xml).root)
+ end
+
+ def children
+ XmlNode::List.new(self)
+ end
+
+ def []=(key, value)
+ @element.attributes[key.to_s] = value.to_xml_value
+ end
+
+ def [](key)
+ @element.attributes[key]
+ end
+
+ # Add a namespace to the node
+ # Example
+ #
+ # node.namespace 'http://www.w3.org/2005/Atom'
+ # node.namespace :opensearch, 'http://a9.com/-/spec/opensearch/1.1/'
+ #
+ def namespace(*args)
+ args[0] = args[0].to_s if args[0].is_a?(Symbol)
+ @element.add_namespace(*args)
+ end
+
+ def cdata=(value)
+ new_cdata = REXML::CData.new( value )
+ @element.children.each do |c|
+ if c.is_a?(REXML::CData)
+ return @element.replace_child(c,new_cdata)
+ end
+ end
+ @element << new_cdata
+ end
+
+ def cdata
+ @element.cdatas.first.to_s
+ end
+
+ def name
+ @element.name
+ end
+
+ def text=(value)
+ @element.text = REXML::Text.new( value )
+ end
+
+ def text
+ @element.text
+ end
+
+ def find(scope, xpath)
+ case scope
+ when :first
+ elem = @element.elements[xpath]
+ elem.nil? ? nil : child_nodes[elem] ||= XmlNode.new(elem)
+ when :all
+ @element.elements.to_a(xpath).collect { |e| child_nodes[e] ||= XmlNode.new(e) }
+ end
+ end
+
+ def <<(elem)
+ case elem
+ when nil then return
+ when Array
+ elem.each { |e| @element << e.to_xml_element }
+ else
+ @element << elem.to_xml_element
+ end
+ end
+
+ def to_xml_element
+ @element
+ end
+
+ def to_s
+ @element.to_s
+ end
+
+ # Use to get pretty formatted xml including DECL
+ # instructions
+ def to_xml
+ xml = []
+ document = REXML::Document.new
+ document << REXML::XMLDecl.new('1.0')
+ document << @element
+ document.write( xml, 0)
+ xml.join
+ end
+
+end
94 lib/vendor/xml_node/test/test_generating.rb
@@ -0,0 +1,94 @@
+require 'rubygems'
+require 'active_support'
+require "test/unit"
+
+require File.dirname(__FILE__) + "/../lib/xml_node"
+
+class TestXmlNode < Test::Unit::TestCase
+
+ def test_init_sanity
+ assert_raise(ArgumentError) { XmlNode.new }
+ assert_nothing_raised { XmlNode.new('feed')}
+ assert_nothing_raised { XmlNode.new('feed', 'content') }
+ assert_nothing_raised { XmlNode.new('feed', :attribute => true) }
+ assert_nothing_raised { XmlNode.new('feed', 'content', :attribute => true) }
+ end
+
+ def test_element_generation
+ assert_equal '<feed/>', XmlNode.new('feed').to_s
+ assert_equal '<feed>content</feed>', XmlNode.new('feed', 'content').to_s
+ assert_equal "<feed attr='true'>content</feed>", XmlNode.new('feed', 'content', :attr => true).to_s
+ assert_equal "<feed attr='true'/>", XmlNode.new('feed', :attr => true).to_s
+ end
+
+ def test_nesting
+ assert_equal '<feed><element/></feed>', XmlNode.new('feed') { |n| n << XmlNode.new('element') }.to_s
+ assert_equal '<feed><element><id>1</id></element></feed>', XmlNode.new('feed') { |n| n << XmlNode.new('element') { |n| n << XmlNode.new('id', '1')} }.to_s
+ end
+
+ def test_cdata
+ node = XmlNode.new('feed')
+ node.text = '...'
+ node.cdata = 'Goodbye world'
+ node.cdata = 'Hello world'
+
+ assert_equal '<feed>...<![CDATA[Hello world]]></feed>', node.to_s
+ assert_equal 'Hello world', node.cdata
+ assert_equal '...', node.text
+ end
+
+ def test_text
+ node = XmlNode.new('feed')
+ node.text = 'Hello world'
+
+ assert_equal '<feed>Hello world</feed>', node.to_s
+ assert_equal 'Hello world', node.text
+ end
+
+ def test_attributes
+ node = XmlNode.new('feed')
+ node['attr'] = 1
+ assert_equal '1', node['attr']
+ end
+
+ def test_namespace
+ node = XmlNode.new('feed')
+ node.namespace 'http://www.w3.org/2005/Atom'
+ assert_equal "<feed xmlns='http://www.w3.org/2005/Atom'/>", node.to_s
+ end