kschadeck-active_shipping 0.9.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/CHANGELOG +38 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.markdown +126 -0
  4. data/lib/active_shipping.rb +50 -0
  5. data/lib/active_shipping/shipping/base.rb +13 -0
  6. data/lib/active_shipping/shipping/carrier.rb +81 -0
  7. data/lib/active_shipping/shipping/carriers.rb +20 -0
  8. data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +16 -0
  9. data/lib/active_shipping/shipping/carriers/canada_post.rb +261 -0
  10. data/lib/active_shipping/shipping/carriers/fedex.rb +372 -0
  11. data/lib/active_shipping/shipping/carriers/kunaki.rb +165 -0
  12. data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +269 -0
  13. data/lib/active_shipping/shipping/carriers/shipwire.rb +178 -0
  14. data/lib/active_shipping/shipping/carriers/ups.rb +452 -0
  15. data/lib/active_shipping/shipping/carriers/usps.rb +441 -0
  16. data/lib/active_shipping/shipping/location.rb +149 -0
  17. data/lib/active_shipping/shipping/package.rb +147 -0
  18. data/lib/active_shipping/shipping/rate_estimate.rb +62 -0
  19. data/lib/active_shipping/shipping/rate_response.rb +19 -0
  20. data/lib/active_shipping/shipping/response.rb +46 -0
  21. data/lib/active_shipping/shipping/shipment_event.rb +14 -0
  22. data/lib/active_shipping/shipping/shipment_packer.rb +48 -0
  23. data/lib/active_shipping/shipping/tracking_response.rb +47 -0
  24. data/lib/active_shipping/version.rb +3 -0
  25. data/lib/certs/eParcel.dtd +111 -0
  26. data/lib/vendor/quantified/MIT-LICENSE +22 -0
  27. data/lib/vendor/quantified/README.markdown +49 -0
  28. data/lib/vendor/quantified/Rakefile +21 -0
  29. data/lib/vendor/quantified/init.rb +0 -0
  30. data/lib/vendor/quantified/lib/quantified.rb +6 -0
  31. data/lib/vendor/quantified/lib/quantified/attribute.rb +208 -0
  32. data/lib/vendor/quantified/lib/quantified/length.rb +20 -0
  33. data/lib/vendor/quantified/lib/quantified/mass.rb +19 -0
  34. data/lib/vendor/quantified/test/length_test.rb +92 -0
  35. data/lib/vendor/quantified/test/mass_test.rb +88 -0
  36. data/lib/vendor/quantified/test/test_helper.rb +10 -0
  37. data/lib/vendor/test_helper.rb +7 -0
  38. data/lib/vendor/xml_node/README +36 -0
  39. data/lib/vendor/xml_node/Rakefile +21 -0
  40. data/lib/vendor/xml_node/benchmark/bench_generation.rb +32 -0
  41. data/lib/vendor/xml_node/init.rb +1 -0
  42. data/lib/vendor/xml_node/lib/xml_node.rb +222 -0
  43. data/lib/vendor/xml_node/test/test_generating.rb +94 -0
  44. data/lib/vendor/xml_node/test/test_parsing.rb +43 -0
  45. metadata +233 -0
@@ -0,0 +1,38 @@
1
+ 2011/04/21:
2
+ * USPS updated to use new APIs [james]
3
+ * new :gift boolean option for Package [james]
4
+ * Location's :address_type can be "po_box" [james]
5
+
6
+ Earlier:
7
+ * New Zealand Post [AbleTech]
8
+ * Include address name for rate requests to Shipwire if provided [dennis]
9
+ * Add support for address name to Location [dennis]
10
+ * Add fix for updated USPS API to strip any encoded html and trailing asterisks from rate names [dennis]
11
+ * Add carrier CanadaPost [william]
12
+ * Update FedEx rates and added ability to auto-generate rate name from code that gets returned by FedEx [dennis]
13
+ * Assume test_helper is in load path when running tests [cody]
14
+ * Add support Kunaki rating service [cody]
15
+ * Require active_support instead of activesupport to avoid deprecation warning in Rails 2.3.5 [cody]
16
+ * Remove ftools for Rails 1.9 compatibility and remove xml logging, as logging is now included in the connection [cody]
17
+ * Update connection code from ActiveMerchant [cody]
18
+ * Fix space-ridden USPS usernames when validating credentials [james]
19
+ * Remove extra slash from USPS URLs [james]
20
+ * Update Shipwire endpoint hostname [cody]
21
+ * Add missing ISO countries [Edward Ocampo-Gooding]
22
+ * Add support for Guernsey to country.rb [cody]
23
+ * Use :words_connector instead of connector in RequiresParameters [cody]
24
+ * Fix extra slash in UPS endpoints [cody]
25
+ * Add name to Shipwire class [cody]
26
+ * Improve FedEx handling of some error conditions [cody]
27
+ * Add support for validating credentials to Shipwire [cody]
28
+ * Add support for ssl_get to PostsData. Update Carriers to use PostsData module. Turn on retry safety for carriers [cody]
29
+ * Add support for Shipwire Shipping Rate API [cody]
30
+ * Cleanup package tests [cody]
31
+ * Remove unused Carrier#setup method [cody]
32
+ * Don't use Array splat in Regex comparisons in Package [cody]
33
+ * Default the Location to use the :alpha2 country code format [cody]
34
+ * Add configurable timeouts from Active Merchant [cody]
35
+ * Update xml_node.rb from XML Node [cody]
36
+ * Update requires_parameters from ActiveMerchant [cody]
37
+ * Sync posts_data.rb with ActiveMerchant [cody]
38
+ * Don't use credentials fixtures in local tests [cody]
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 James MacAulay
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,126 @@
1
+ # Active Shipping
2
+
3
+ This library interfaces 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.
4
+
5
+ Active Shipping is currently being used and improved in a production environment for [Shopify][]. Development is being done by the Shopify integrations team (<integrations-team@shopify.com>). Discussion is welcome in the [Active Merchant Google Group][discuss].
6
+
7
+ [Active Merchant]:http://www.activemerchant.org
8
+ [Shopify]:http://www.shopify.com
9
+ [discuss]:http://groups.google.com/group/activemerchant
10
+
11
+ ## Supported Shipping Carriers
12
+
13
+ * [UPS](http://www.ups.com)
14
+ * [USPS](http://www.usps.com)
15
+ * [FedEx](http://www.fedex.com)
16
+ * [Canada Post](http://www.canadapost.ca)
17
+ * [New Zealand Post](http://www.nzpost.co.nz)
18
+ * more soon!
19
+
20
+ ## Installation
21
+
22
+ gem install active_shipping
23
+
24
+ ...or add it to your [Gemfile](http://gembundler.com/).
25
+
26
+ ## Sample Usage
27
+
28
+ ### Compare rates from different carriers
29
+
30
+ require 'active_shipping'
31
+ include ActiveMerchant::Shipping
32
+
33
+ # Package up a poster and a Wii for your nephew.
34
+ packages = [
35
+ Package.new( 100, # 100 grams
36
+ [93,10], # 93 cm long, 10 cm diameter
37
+ :cylinder => true), # cylinders have different volume calculations
38
+
39
+ Package.new( (7.5 * 16), # 7.5 lbs, times 16 oz/lb.
40
+ [15, 10, 4.5], # 15x10x4.5 inches
41
+ :units => :imperial) # not grams, not centimetres
42
+ ]
43
+
44
+ # You live in Beverly Hills, he lives in Ottawa
45
+ origin = Location.new( :country => 'US',
46
+ :state => 'CA',
47
+ :city => 'Beverly Hills',
48
+ :zip => '90210')
49
+
50
+ destination = Location.new( :country => 'CA',
51
+ :province => 'ON',
52
+ :city => 'Ottawa',
53
+ :postal_code => 'K1P 1J1')
54
+
55
+ # Find out how much it'll be.
56
+ ups = UPS.new(:login => 'auntjudy', :password => 'secret', :key => 'xml-access-key')
57
+ response = ups.find_rates(origin, destination, packages)
58
+
59
+ ups_rates = response.rates.sort_by(&:price).collect {|rate| [rate.service_name, rate.price]}
60
+ # => [["UPS Standard", 3936],
61
+ # ["UPS Worldwide Expedited", 8682],
62
+ # ["UPS Saver", 9348],
63
+ # ["UPS Express", 9702],
64
+ # ["UPS Worldwide Express Plus", 14502]]
65
+
66
+ # Check out USPS for comparison...
67
+ usps = USPS.new(:login => 'developer-key')
68
+ response = usps.find_rates(origin, destination, packages)
69
+
70
+ usps_rates = response.rates.sort_by(&:price).collect {|rate| [rate.service_name, rate.price]}
71
+ # => [["USPS Priority Mail International", 4110],
72
+ # ["USPS Express Mail International (EMS)", 5750],
73
+ # ["USPS Global Express Guaranteed Non-Document Non-Rectangular", 9400],
74
+ # ["USPS GXG Envelopes", 9400],
75
+ # ["USPS Global Express Guaranteed Non-Document Rectangular", 9400],
76
+ # ["USPS Global Express Guaranteed", 9400]]
77
+
78
+ ### Track a FedEx package
79
+
80
+ fedex = FedEx.new(:login => '999999999', :password => '7777777')
81
+ tracking_info = fedex.find_tracking_info('tracking-number', :carrier_code => 'fedex_ground') # Ground package
82
+
83
+ tracking_info.shipment_events.each do |event|
84
+ puts "#{event.name} at #{event.location.city}, #{event.location.state} on #{event.time}. #{event.message}"
85
+ end
86
+ # => Package information transmitted to FedEx at NASHVILLE LOCAL, TN on Thu Oct 23 00:00:00 UTC 2008.
87
+ # Picked up by FedEx at NASHVILLE LOCAL, TN on Thu Oct 23 17:30:00 UTC 2008.
88
+ # Scanned at FedEx sort facility at NASHVILLE, TN on Thu Oct 23 18:50:00 UTC 2008.
89
+ # Departed FedEx sort facility at NASHVILLE, TN on Thu Oct 23 22:33:00 UTC 2008.
90
+ # Arrived at FedEx sort facility at KNOXVILLE, TN on Fri Oct 24 02:45:00 UTC 2008.
91
+ # Scanned at FedEx sort facility at KNOXVILLE, TN on Fri Oct 24 05:56:00 UTC 2008.
92
+ # Delivered at Knoxville, TN on Fri Oct 24 16:45:00 UTC 2008. Signed for by: T.BAKER
93
+
94
+ ## Running the tests
95
+
96
+ After installing dependencies with `bundle install`, you can run the unit tests with `rake test:units` and the remote tests with `rake test:remote`. The unit tests mock out requests and responses so that everything runs locally, while the remote tests actually hit the carrier servers. For the remote tests, you'll need valid test credentials for any carriers' tests you want to run. The credentials should go in ~/.active_merchant/fixtures.yml, and the format of that file can be seen in the included [fixtures.yml](https://github.com/Shopify/active_shipping/blob/master/test/fixtures.yml).
97
+
98
+ ## Contributing
99
+
100
+ 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.
101
+
102
+ For the features you add, you should have both unit tests and remote tests. It's probably best to start with the remote tests, and then log those requests and responses and use them as the mocks for the unit tests. You can see how this works with the USPS tests right now:
103
+
104
+ https://github.com/Shopify/active_shipping/blob/master/test/remote/usps_test.rb
105
+ https://github.com/Shopify/active_shipping/blob/master/test/unit/carriers/usps_test.rb
106
+ https://github.com/Shopify/active_shipping/tree/master/test/fixtures/xml/usps
107
+
108
+ To log requests and responses, just set the `logger` on your carrier class to some kind of `Logger` object:
109
+
110
+ USPS.logger = Logger.new($stdout)
111
+
112
+ (This logging functionality is provided by the [`PostsData` module](https://github.com/Shopify/active_utils/blob/master/lib/active_utils/common/posts_data.rb) in the `active_utils` dependency.)
113
+
114
+ After you've pushed your well-tested changes to your github fork, make a pull request and we'll take it from there!
115
+
116
+ ## Contributors
117
+
118
+ * James MacAulay (<http://jmacaulay.net>)
119
+ * Tobias Luetke (<http://blog.leetsoft.com>)
120
+ * Cody Fauser (<http://codyfauser.com>)
121
+ * Jimmy Baker (<http://jimmyville.com/>)
122
+ * William Lang (<http://williamlang.net/>)
123
+
124
+ ## Legal Mumbo Jumbo
125
+
126
+ 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.
@@ -0,0 +1,50 @@
1
+ #--
2
+ # Copyright (c) 2009 Jaded Pixel
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ $:.unshift File.dirname(__FILE__)
25
+
26
+ begin
27
+ require 'active_support/all'
28
+ rescue LoadError => e
29
+ require 'rubygems'
30
+ gem "activesupport", ">= 2.3.5"
31
+ require "active_support/all"
32
+ end
33
+
34
+ autoload :XmlNode, 'vendor/xml_node/lib/xml_node'
35
+ autoload :Quantified, 'vendor/quantified/lib/quantified'
36
+
37
+ require 'net/https'
38
+ require 'active_utils'
39
+
40
+ require 'active_shipping/shipping/base'
41
+ require 'active_shipping/shipping/response'
42
+ require 'active_shipping/shipping/rate_response'
43
+ require 'active_shipping/shipping/tracking_response'
44
+ require 'active_shipping/shipping/package'
45
+ require 'active_shipping/shipping/location'
46
+ require 'active_shipping/shipping/rate_estimate'
47
+ require 'active_shipping/shipping/shipment_event'
48
+ require 'active_shipping/shipping/shipment_packer'
49
+ require 'active_shipping/shipping/carrier'
50
+ require 'active_shipping/shipping/carriers'
@@ -0,0 +1,13 @@
1
+ module ActiveMerchant
2
+ module Shipping
3
+ module Base
4
+ mattr_accessor :mode
5
+ self.mode = :production
6
+
7
+ def self.carrier(name)
8
+ ActiveMerchant::Shipping::Carriers.all.find {|c| c.name.downcase == name.to_s.downcase} ||
9
+ raise(NameError, "unknown carrier #{name}")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,81 @@
1
+ module ActiveMerchant
2
+ module Shipping
3
+ class Carrier
4
+
5
+ include RequiresParameters
6
+ include PostsData
7
+ include Quantified
8
+
9
+ attr_reader :last_request
10
+ attr_accessor :test_mode
11
+ alias_method :test_mode?, :test_mode
12
+
13
+ # Credentials should be in options hash under keys :login, :password and/or :key.
14
+ def initialize(options = {})
15
+ requirements.each {|key| requires!(options, key)}
16
+ @options = options
17
+ @last_request = nil
18
+ @test_mode = @options[:test]
19
+ end
20
+
21
+ # Override to return required keys in options hash for initialize method.
22
+ def requirements
23
+ []
24
+ end
25
+
26
+ # Override with whatever you need to get the rates
27
+ def find_rates(origin, destination, packages, options = {})
28
+ end
29
+
30
+ # Validate credentials with a call to the API. By default this just does a find_rates call
31
+ # with the orgin and destination both as the carrier's default_location. Override to provide
32
+ # alternate functionality, such as checking for test_mode to use test servers, etc.
33
+ def valid_credentials?
34
+ location = self.class.default_location
35
+ find_rates(location,location,Package.new(100, [5,15,30]), :test => test_mode)
36
+ rescue ActiveMerchant::Shipping::ResponseError
37
+ false
38
+ else
39
+ true
40
+ end
41
+
42
+ def maximum_weight
43
+ Mass.new(150, :pounds)
44
+ end
45
+
46
+ protected
47
+
48
+ def node_text_or_nil(xml_node)
49
+ xml_node ? xml_node.text : nil
50
+ end
51
+
52
+ # Override in subclasses for non-U.S.-based carriers.
53
+ def self.default_location
54
+ Location.new( :country => 'US',
55
+ :state => 'CA',
56
+ :city => 'Beverly Hills',
57
+ :address1 => '455 N. Rexford Dr.',
58
+ :address2 => '3rd Floor',
59
+ :zip => '90210',
60
+ :phone => '1-310-285-1013',
61
+ :fax => '1-310-275-8159')
62
+ end
63
+
64
+ # Use after building the request to save for later inspection. Probably won't ever be overridden.
65
+ def save_request(r)
66
+ @last_request = r
67
+ end
68
+
69
+ def timestamp_from_business_day(days)
70
+ return unless days
71
+ date = DateTime.now
72
+ days.times do
73
+ begin
74
+ date = date + 1
75
+ end until ![0,6].include?(date.wday)
76
+ end
77
+ date
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,20 @@
1
+ require 'active_shipping/shipping/carriers/bogus_carrier'
2
+ require 'active_shipping/shipping/carriers/ups'
3
+ require 'active_shipping/shipping/carriers/usps'
4
+ require 'active_shipping/shipping/carriers/fedex'
5
+ require 'active_shipping/shipping/carriers/shipwire'
6
+ require 'active_shipping/shipping/carriers/kunaki'
7
+ require 'active_shipping/shipping/carriers/canada_post'
8
+ require 'active_shipping/shipping/carriers/new_zealand_post'
9
+
10
+ module ActiveMerchant
11
+ module Shipping
12
+ module Carriers
13
+ class <<self
14
+ def all
15
+ [BogusCarrier, UPS, USPS, FedEx, Shipwire, Kunaki, CanadaPost, NewZealandPost]
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ module ActiveMerchant
2
+ module Shipping
3
+ class BogusCarrier < Carrier
4
+ cattr_reader :name
5
+ @@name = "Bogus Carrier"
6
+
7
+
8
+ def find_rates(origin, destination, packages, options = {})
9
+ origin = Location.from(origin)
10
+ destination = Location.from(destination)
11
+ packages = Array(packages)
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,261 @@
1
+ require 'cgi'
2
+
3
+ module ActiveMerchant
4
+ module Shipping
5
+
6
+ class CanadaPost < Carrier
7
+
8
+ # NOTE!
9
+ # A Merchant CPC Id must be assigned to you by Canada Post
10
+ # CPC_DEMO_XML is just a public domain account for testing
11
+
12
+ class CanadaPostRateResponse < RateResponse
13
+
14
+ attr_reader :boxes, :postal_outlets
15
+
16
+ def initialize(success, message, params = {}, options = {})
17
+ @boxes = options[:boxes]
18
+ @postal_outlets = options[:postal_outlets]
19
+ super
20
+ end
21
+
22
+ end
23
+
24
+ cattr_reader :name, :name_french
25
+ @@name = "Canada Post"
26
+ @@name_french = "Postes Canada"
27
+
28
+ Box = Struct.new(:name, :weight, :expediter_weight, :length, :width, :height, :packedItems)
29
+ PackedItem = Struct.new(:quantity, :description)
30
+ PostalOutlet = Struct.new(:sequence_no, :distance, :name, :business_name, :postal_address, :business_hours)
31
+
32
+ DEFAULT_TURN_AROUND_TIME = 24
33
+ URL = "http://sellonline.canadapost.ca:30000"
34
+ DOCTYPE = '<!DOCTYPE eparcel SYSTEM "http://sellonline.canadapost.ca/DevelopersResources/protocolV3/eParcel.dtd">'
35
+
36
+ RESPONSE_CODES = {
37
+ '1' => "All calculation was done",
38
+ '2' => "Default shipping rates are returned due to a problem during the processing of the request.",
39
+ '-2' => "Missing argument when calling module",
40
+ '-5' => "No Item to ship",
41
+ '-6' => "Illegal Item weight",
42
+ '-7' => "Illegal item dimension",
43
+ '-12' => "Can't open IM config file",
44
+ '-13' => "Can't create log files",
45
+ '-15' => "Invalid config file format",
46
+ '-102' => "Invalid socket connection",
47
+ '-106' => "Can't connect to server",
48
+ '-1000' => "Unknow request type sent by client",
49
+ '-1002' => "MAS Timed out",
50
+ '-1004' => "Socket communication break",
51
+ '-1005' => "Did not receive required data on socket.",
52
+ '-2000' => "Unable to estabish socket connection with RSSS",
53
+ '-2001' => "Merchant Id not found on server",
54
+ '-2002' => "One or more parameter was not sent by the IM to the MAS",
55
+ '-2003' => "Did not receive required data on socket.",
56
+ '-2004' => "The request contains to many items to process it.",
57
+ '-2005' => "The request received on socket is larger than the maximum allowed.",
58
+ '-3000' => "Origin Postal Code is illegal",
59
+ '-3001' => "Destination Postal Code/State Name/ Country is illegal",
60
+ '-3002' => "Parcel too large to be shipped with CPC",
61
+ '-3003' => "Parcel too small to be shipped with CPC",
62
+ '-3004' => "Parcel too heavy to be shipped with CPC",
63
+ '-3005' => "Internal error code returned by the rating DLL",
64
+ '-3006' => "The pick up time format is invalid or not defined.",
65
+ '-4000' => "Volumetric internal error",
66
+ '-4001' => "Volumetric time out calculation error.",
67
+ '-4002' => "No bins provided to the volumetric engine.",
68
+ '-4003' => "No items provided to the volumetric engine.",
69
+ '-4004' => "Item is too large to be packed",
70
+ '-4005' => "Number of item more than maximum allowed",
71
+ '-5000' => "XML Parsing error",
72
+ '-5001' => "XML Tag not found",
73
+ '-5002' => "Node Value Number format error",
74
+ '-5003' => "Node value is empty",
75
+ '-5004' => "Unable to create/parse XML Document",
76
+ '-6000' => "Unable to open the database",
77
+ '-6001' => "Unable to read from the database",
78
+ '-6002' => "Unable to write to the database",
79
+ '-50000' => "Internal problem - Please contact Sell Online Help Desk"
80
+ }
81
+
82
+ NON_ISO_COUNTRY_NAMES = {
83
+ 'Russian Federation' => 'Russia'
84
+ }
85
+
86
+
87
+ def requirements
88
+ [:login]
89
+ end
90
+
91
+ def find_rates(origin, destination, line_items = [], options = {})
92
+ rate_request = build_rate_request(origin, destination, line_items, options)
93
+ commit(rate_request, origin, destination, options)
94
+ end
95
+
96
+ def maximum_weight
97
+ Mass.new(30, :kilograms)
98
+ end
99
+
100
+ def self.default_location
101
+ {
102
+ :country => 'CA',
103
+ :province => 'ON',
104
+ :city => 'Ottawa',
105
+ :address1 => '61A York St',
106
+ :postal_code => 'K1N5T2'
107
+ }
108
+ end
109
+
110
+ protected
111
+
112
+ def commit(request, origin, destination, options = {})
113
+ response = parse_rate_response(ssl_post(URL, request), origin, destination, options)
114
+ end
115
+
116
+ private
117
+
118
+ def build_rate_request(origin, destination, line_items = [], options = {})
119
+ line_items = [line_items] if !line_items.is_a?(Array)
120
+ origin = origin.is_a?(Location) ? origin : Location.new(origin)
121
+ destination = destination.is_a?(Location) ? destination : Location.new(destination)
122
+
123
+ xml_request = XmlNode.new('eparcel') do |root_node|
124
+ root_node << XmlNode.new('language', @options[:french] ? 'fr' : 'en')
125
+ root_node << XmlNode.new('ratesAndServicesRequest') do |request|
126
+
127
+ request << XmlNode.new('merchantCPCID', @options[:login])
128
+ request << XmlNode.new('fromPostalCode', origin.postal_code)
129
+ request << XmlNode.new('turnAroundTime', options[:turn_around_time] ? options[:turn_around_time] : DEFAULT_TURN_AROUND_TIME)
130
+ request << XmlNode.new('itemsPrice', dollar_amount(line_items.sum(&:value)))
131
+
132
+ #line items
133
+ request << build_line_items(line_items)
134
+
135
+ #delivery info
136
+ #NOTE: These tags MUST be after line items
137
+ request << XmlNode.new('city', destination.city)
138
+ request << XmlNode.new('provOrState', destination.province)
139
+ request << XmlNode.new('country', handle_non_iso_country_names(destination.country))
140
+ request << XmlNode.new('postalCode', destination.postal_code)
141
+ end
142
+ end
143
+
144
+ DOCTYPE + xml_request.to_s
145
+ end
146
+
147
+ def parse_rate_response(response, origin, destination, options = {})
148
+ xml = REXML::Document.new(response)
149
+ success = response_success?(xml)
150
+ message = response_message(xml)
151
+
152
+ rate_estimates = []
153
+ boxes = []
154
+ if success
155
+ xml.elements.each('eparcel/ratesAndServicesResponse/product') do |product|
156
+ service_name = (@options[:french] ? @@name_french : @@name) + " " + product.get_text('name').to_s
157
+ service_code = product.attribute('id').to_s
158
+
159
+ rate_estimates << RateEstimate.new(origin, destination, @@name, service_name,
160
+ :service_code => service_code,
161
+ :total_price => product.get_text('rate').to_s,
162
+ :currency => 'CAD',
163
+ :delivery_range => [product.get_text('deliveryDate').to_s] * 2
164
+ )
165
+ end
166
+
167
+ boxes = xml.elements.collect('eparcel/ratesAndServicesResponse/packing/box') do |box|
168
+ b = Box.new
169
+ b.packedItems = []
170
+ b.name = box.get_text('name').to_s
171
+ b.weight = box.get_text('weight').to_s.to_f
172
+ b.expediter_weight = box.get_text('expediterWeight').to_s.to_f
173
+ b.length = box.get_text('length').to_s.to_f
174
+ b.width = box.get_text('width').to_s.to_f
175
+ b.height = box.get_text('height').to_s.to_f
176
+ b.packedItems = box.elements.collect('packedItem') do |item|
177
+ p = PackedItem.new
178
+ p.quantity = item.get_text('quantity').to_s.to_i
179
+ p.description = item.get_text('description').to_s
180
+ p
181
+ end
182
+ b
183
+ end
184
+
185
+ postal_outlets = xml.elements.collect('eparcel/ratesAndServicesResponse/nearestPostalOutlet') do |outlet|
186
+ postal_outlet = PostalOutlet.new
187
+ postal_outlet.sequence_no = outlet.get_text('postalOutletSequenceNo').to_s
188
+ postal_outlet.distance = outlet.get_text('distance').to_s
189
+ postal_outlet.name = outlet.get_text('outletName').to_s
190
+ postal_outlet.business_name = outlet.get_text('businessName').to_s
191
+
192
+ postal_outlet.postal_address = Location.new({
193
+ :address1 => outlet.get_text('postalAddress/addressLine').to_s,
194
+ :postal_code => outlet.get_text('postalAddress/postal_code').to_s,
195
+ :city => outlet.get_text('postalAddress/municipality').to_s,
196
+ :province => outlet.get_text('postalAddress/province').to_s,
197
+ :country => 'Canada',
198
+ :phone_number => outlet.get_text('phoneNumber').to_s
199
+ })
200
+
201
+ postal_outlet.business_hours = outlet.elements.collect('businessHours') do |hour|
202
+ { :day_of_week => hour.get_text('dayOfWeek').to_s, :time => hour.get_text('time').to_s }
203
+ end
204
+
205
+ postal_outlet
206
+ end
207
+ end
208
+
209
+ CanadaPostRateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :boxes => boxes, :postal_outlets => postal_outlets)
210
+ end
211
+
212
+ def response_success?(xml)
213
+ value = xml.get_text('eparcel/ratesAndServicesResponse/statusCode').to_s
214
+ value == '1' || value == '2'
215
+ end
216
+
217
+ def response_message(xml)
218
+ xml.get_text('eparcel/ratesAndServicesResponse/statusMessage').to_s
219
+ end
220
+
221
+ # <!-- List of items in the shopping -->
222
+ # <!-- cart -->
223
+ # <!-- Each item is defined by : -->
224
+ # <!-- - quantity (mandatory) -->
225
+ # <!-- - size (mandatory) -->
226
+ # <!-- - weight (mandatory) -->
227
+ # <!-- - description (mandatory) -->
228
+ # <!-- - ready to ship (optional) -->
229
+
230
+ def build_line_items(line_items)
231
+ xml_line_items = XmlNode.new('lineItems') do |line_items_node|
232
+
233
+ line_items.each do |line_item|
234
+
235
+ line_items_node << XmlNode.new('item') do |item|
236
+ item << XmlNode.new('quantity', 1)
237
+ item << XmlNode.new('weight', line_item.kilograms)
238
+ item << XmlNode.new('length', line_item.cm(:length).to_s)
239
+ item << XmlNode.new('width', line_item.cm(:width).to_s)
240
+ item << XmlNode.new('height', line_item.cm(:height).to_s)
241
+ item << XmlNode.new('description', line_item.options[:description] || ' ')
242
+ item << XmlNode.new('readyToShip', line_item.options[:ready_to_ship] || nil)
243
+
244
+ # By setting the 'readyToShip' tag to true, Sell Online will not pack this item in the boxes defined in the merchant profile.
245
+ end
246
+ end
247
+ end
248
+
249
+ xml_line_items
250
+ end
251
+
252
+ def dollar_amount(cents)
253
+ "%0.2f" % (cents / 100.0)
254
+ end
255
+
256
+ def handle_non_iso_country_names(country)
257
+ NON_ISO_COUNTRY_NAMES[country.to_s] || country
258
+ end
259
+ end
260
+ end
261
+ end