benhutton-active_shipping 0.9.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/CHANGELOG +38 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.markdown +142 -0
  4. data/lib/active_merchant/common.rb +14 -0
  5. data/lib/active_merchant/common/connection.rb +177 -0
  6. data/lib/active_merchant/common/country.rb +328 -0
  7. data/lib/active_merchant/common/error.rb +26 -0
  8. data/lib/active_merchant/common/post_data.rb +24 -0
  9. data/lib/active_merchant/common/posts_data.rb +63 -0
  10. data/lib/active_merchant/common/requires_parameters.rb +16 -0
  11. data/lib/active_merchant/common/utils.rb +22 -0
  12. data/lib/active_merchant/common/validateable.rb +76 -0
  13. data/lib/active_shipping.rb +49 -0
  14. data/lib/active_shipping/shipping/base.rb +13 -0
  15. data/lib/active_shipping/shipping/carrier.rb +70 -0
  16. data/lib/active_shipping/shipping/carriers.rb +20 -0
  17. data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +16 -0
  18. data/lib/active_shipping/shipping/carriers/canada_post.rb +268 -0
  19. data/lib/active_shipping/shipping/carriers/fedex.rb +331 -0
  20. data/lib/active_shipping/shipping/carriers/kunaki.rb +165 -0
  21. data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +139 -0
  22. data/lib/active_shipping/shipping/carriers/shipwire.rb +172 -0
  23. data/lib/active_shipping/shipping/carriers/ups.rb +390 -0
  24. data/lib/active_shipping/shipping/carriers/usps.rb +441 -0
  25. data/lib/active_shipping/shipping/location.rb +109 -0
  26. data/lib/active_shipping/shipping/package.rb +147 -0
  27. data/lib/active_shipping/shipping/rate_estimate.rb +54 -0
  28. data/lib/active_shipping/shipping/rate_response.rb +19 -0
  29. data/lib/active_shipping/shipping/response.rb +46 -0
  30. data/lib/active_shipping/shipping/shipment_event.rb +14 -0
  31. data/lib/active_shipping/shipping/tracking_response.rb +22 -0
  32. data/lib/active_shipping/version.rb +3 -0
  33. data/lib/certs/cacert.pem +7815 -0
  34. data/lib/certs/eParcel.dtd +111 -0
  35. data/lib/vendor/quantified/MIT-LICENSE +22 -0
  36. data/lib/vendor/quantified/README.markdown +49 -0
  37. data/lib/vendor/quantified/Rakefile +21 -0
  38. data/lib/vendor/quantified/init.rb +0 -0
  39. data/lib/vendor/quantified/lib/quantified.rb +8 -0
  40. data/lib/vendor/quantified/lib/quantified/attribute.rb +208 -0
  41. data/lib/vendor/quantified/lib/quantified/length.rb +20 -0
  42. data/lib/vendor/quantified/lib/quantified/mass.rb +19 -0
  43. data/lib/vendor/quantified/test/length_test.rb +92 -0
  44. data/lib/vendor/quantified/test/mass_test.rb +88 -0
  45. data/lib/vendor/quantified/test/test_helper.rb +2 -0
  46. data/lib/vendor/test_helper.rb +13 -0
  47. data/lib/vendor/xml_node/README +36 -0
  48. data/lib/vendor/xml_node/Rakefile +21 -0
  49. data/lib/vendor/xml_node/benchmark/bench_generation.rb +32 -0
  50. data/lib/vendor/xml_node/init.rb +1 -0
  51. data/lib/vendor/xml_node/lib/xml_node.rb +222 -0
  52. data/lib/vendor/xml_node/test/test_generating.rb +94 -0
  53. data/lib/vendor/xml_node/test/test_parsing.rb +43 -0
  54. metadata +125 -0
@@ -0,0 +1,26 @@
1
+ module ActiveMerchant #:nodoc:
2
+ class ActiveMerchantError < StandardError #:nodoc:
3
+ end
4
+
5
+ class ConnectionError < ActiveMerchantError # :nodoc:
6
+ end
7
+
8
+ class RetriableConnectionError < ConnectionError # :nodoc:
9
+ end
10
+
11
+ class ResponseError < ActiveMerchantError # :nodoc:
12
+ attr_reader :response
13
+
14
+ def initialize(response, message = nil)
15
+ @response = response
16
+ @message = message
17
+ end
18
+
19
+ def to_s
20
+ "Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
21
+ end
22
+ end
23
+
24
+ class ClientCertificateError < ActiveMerchantError # :nodoc
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ require 'cgi'
2
+
3
+ module ActiveMerchant
4
+ class PostData < Hash
5
+ class_inheritable_accessor :required_fields, :instance_writer => false
6
+ self.required_fields = []
7
+
8
+ def []=(key, value)
9
+ return if value.blank? && !required?(key)
10
+ super
11
+ end
12
+
13
+ def to_post_data
14
+ collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
15
+ end
16
+
17
+ alias_method :to_s, :to_post_data
18
+
19
+ private
20
+ def required?(key)
21
+ required_fields.include?(key)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module PostsData #:nodoc:
3
+
4
+ def self.included(base)
5
+ base.superclass_delegating_accessor :ssl_strict
6
+ base.ssl_strict = true
7
+
8
+ base.class_inheritable_accessor :retry_safe
9
+ base.retry_safe = false
10
+
11
+ base.superclass_delegating_accessor :open_timeout
12
+ base.open_timeout = 60
13
+
14
+ base.superclass_delegating_accessor :read_timeout
15
+ base.read_timeout = 60
16
+
17
+ base.superclass_delegating_accessor :logger
18
+ base.superclass_delegating_accessor :wiredump_device
19
+ end
20
+
21
+ def ssl_get(endpoint, headers={})
22
+ ssl_request(:get, endpoint, nil, headers)
23
+ end
24
+
25
+ def ssl_post(endpoint, data, headers = {})
26
+ ssl_request(:post, endpoint, data, headers)
27
+ end
28
+
29
+ def ssl_request(method, endpoint, data, headers)
30
+ handle_response(raw_ssl_request(method, endpoint, data, headers))
31
+ end
32
+
33
+ def raw_ssl_request(method, endpoint, data, headers = {})
34
+ connection = Connection.new(endpoint)
35
+ connection.open_timeout = open_timeout
36
+ connection.read_timeout = read_timeout
37
+ connection.retry_safe = retry_safe
38
+ connection.verify_peer = ssl_strict
39
+ connection.logger = logger
40
+ connection.tag = self.class.name
41
+ connection.wiredump_device = wiredump_device
42
+
43
+ connection.pem = @options[:pem] if @options
44
+ connection.pem_password = @options[:pem_password] if @options
45
+
46
+ connection.ignore_http_status = @options[:ignore_http_status] if @options
47
+
48
+ connection.request(method, data, headers)
49
+ end
50
+
51
+ private
52
+
53
+ def handle_response(response)
54
+ case response.code.to_i
55
+ when 200...300
56
+ response.body
57
+ else
58
+ raise ResponseError.new(response)
59
+ end
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,16 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module RequiresParameters #:nodoc:
3
+ def requires!(hash, *params)
4
+ params.each do |param|
5
+ if param.is_a?(Array)
6
+ raise ArgumentError.new("Missing required parameter: #{param.first}") unless hash.has_key?(param.first)
7
+
8
+ valid_options = param[1..-1]
9
+ raise ArgumentError.new("Parameter: #{param.first} must be one of #{valid_options.to_sentence(:words_connector => 'or')}") unless valid_options.include?(hash[param.first])
10
+ else
11
+ raise ArgumentError.new("Missing required parameter: #{param}") unless hash.has_key?(param)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ require 'digest/md5'
2
+
3
+ module ActiveMerchant #:nodoc:
4
+ module Utils #:nodoc:
5
+ def generate_unique_id
6
+ md5 = Digest::MD5.new
7
+ now = Time.now
8
+ md5 << now.to_s
9
+ md5 << String(now.usec)
10
+ md5 << String(rand(0))
11
+ md5 << String($$)
12
+ md5 << self.class.name
13
+ md5.hexdigest
14
+ end
15
+
16
+ module_function :generate_unique_id
17
+
18
+ def deprecated(message)
19
+ warn(Kernel.caller[1] + message)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,76 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Validateable #:nodoc:
3
+ def valid?
4
+ errors.clear
5
+
6
+ before_validate if respond_to?(:before_validate, true)
7
+ validate if respond_to?(:validate, true)
8
+
9
+ errors.empty?
10
+ end
11
+
12
+ def initialize(attributes = {})
13
+ self.attributes = attributes
14
+ end
15
+
16
+ def errors
17
+ @errors ||= Errors.new(self)
18
+ end
19
+
20
+ private
21
+
22
+ def attributes=(attributes)
23
+ unless attributes.nil?
24
+ for key, value in attributes
25
+ send("#{key}=", value )
26
+ end
27
+ end
28
+ end
29
+
30
+ # This hash keeps the errors of the object
31
+ class Errors < HashWithIndifferentAccess
32
+
33
+ def initialize(base)
34
+ @base = base
35
+ end
36
+
37
+ def count
38
+ size
39
+ end
40
+
41
+ # returns a specific fields error message.
42
+ # if more than one error is available we will only return the first. If no error is available
43
+ # we return an empty string
44
+ def on(field)
45
+ self[field].to_a.first
46
+ end
47
+
48
+ def add(field, error)
49
+ self[field] ||= []
50
+ self[field] << error
51
+ end
52
+
53
+ def add_to_base(error)
54
+ add(:base, error)
55
+ end
56
+
57
+ def each_full
58
+ full_messages.each { |msg| yield msg }
59
+ end
60
+
61
+ def full_messages
62
+ result = []
63
+
64
+ self.each do |key, messages|
65
+ if key == 'base'
66
+ result << "#{messages.first}"
67
+ else
68
+ result << "#{key.to_s.humanize} #{messages.first}"
69
+ end
70
+ end
71
+
72
+ result
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,49 @@
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_merchant/common'
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/carrier'
49
+ 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,70 @@
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
+ end
69
+ end
70
+ 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,268 @@
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
+ delivery_date = date_for(product.get_text('deliveryDate').to_s)
159
+
160
+ rate_estimates << RateEstimate.new(origin, destination, @@name, service_name,
161
+ :service_code => service_code,
162
+ :total_price => product.get_text('rate').to_s,
163
+ :delivery_date => delivery_date,
164
+ :currency => 'CAD'
165
+ )
166
+ end
167
+
168
+ boxes = xml.elements.collect('eparcel/ratesAndServicesResponse/packing/box') do |box|
169
+ b = Box.new
170
+ b.packedItems = []
171
+ b.name = box.get_text('name').to_s
172
+ b.weight = box.get_text('weight').to_s.to_f
173
+ b.expediter_weight = box.get_text('expediterWeight').to_s.to_f
174
+ b.length = box.get_text('length').to_s.to_f
175
+ b.width = box.get_text('width').to_s.to_f
176
+ b.height = box.get_text('height').to_s.to_f
177
+ b.packedItems = box.elements.collect('packedItem') do |item|
178
+ p = PackedItem.new
179
+ p.quantity = item.get_text('quantity').to_s.to_i
180
+ p.description = item.get_text('description').to_s
181
+ p
182
+ end
183
+ b
184
+ end
185
+
186
+ postal_outlets = xml.elements.collect('eparcel/ratesAndServicesResponse/nearestPostalOutlet') do |outlet|
187
+ postal_outlet = PostalOutlet.new
188
+ postal_outlet.sequence_no = outlet.get_text('postalOutletSequenceNo').to_s
189
+ postal_outlet.distance = outlet.get_text('distance').to_s
190
+ postal_outlet.name = outlet.get_text('outletName').to_s
191
+ postal_outlet.business_name = outlet.get_text('businessName').to_s
192
+
193
+ postal_outlet.postal_address = Location.new({
194
+ :address1 => outlet.get_text('postalAddress/addressLine').to_s,
195
+ :postal_code => outlet.get_text('postalAddress/postal_code').to_s,
196
+ :city => outlet.get_text('postalAddress/municipality').to_s,
197
+ :province => outlet.get_text('postalAddress/province').to_s,
198
+ :country => 'Canada',
199
+ :phone_number => outlet.get_text('phoneNumber').to_s
200
+ })
201
+
202
+ postal_outlet.business_hours = outlet.elements.collect('businessHours') do |hour|
203
+ { :day_of_week => hour.get_text('dayOfWeek').to_s, :time => hour.get_text('time').to_s }
204
+ end
205
+
206
+ postal_outlet
207
+ end
208
+ end
209
+
210
+ CanadaPostRateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :boxes => boxes, :postal_outlets => postal_outlets)
211
+ end
212
+
213
+ def date_for(string)
214
+ string && Time.parse(string)
215
+ rescue ArgumentError
216
+ nil
217
+ end
218
+
219
+ def response_success?(xml)
220
+ value = xml.get_text('eparcel/ratesAndServicesResponse/statusCode').to_s
221
+ value == '1' || value == '2'
222
+ end
223
+
224
+ def response_message(xml)
225
+ xml.get_text('eparcel/ratesAndServicesResponse/statusMessage').to_s
226
+ end
227
+
228
+ # <!-- List of items in the shopping -->
229
+ # <!-- cart -->
230
+ # <!-- Each item is defined by : -->
231
+ # <!-- - quantity (mandatory) -->
232
+ # <!-- - size (mandatory) -->
233
+ # <!-- - weight (mandatory) -->
234
+ # <!-- - description (mandatory) -->
235
+ # <!-- - ready to ship (optional) -->
236
+
237
+ def build_line_items(line_items)
238
+ xml_line_items = XmlNode.new('lineItems') do |line_items_node|
239
+
240
+ line_items.each do |line_item|
241
+
242
+ line_items_node << XmlNode.new('item') do |item|
243
+ item << XmlNode.new('quantity', 1)
244
+ item << XmlNode.new('weight', line_item.kilograms)
245
+ item << XmlNode.new('length', line_item.cm(:length).to_s)
246
+ item << XmlNode.new('width', line_item.cm(:width).to_s)
247
+ item << XmlNode.new('height', line_item.cm(:height).to_s)
248
+ item << XmlNode.new('description', line_item.options[:description] || ' ')
249
+ item << XmlNode.new('readyToShip', line_item.options[:ready_to_ship] || nil)
250
+
251
+ # By setting the 'readyToShip' tag to true, Sell Online will not pack this item in the boxes defined in the merchant profile.
252
+ end
253
+ end
254
+ end
255
+
256
+ xml_line_items
257
+ end
258
+
259
+ def dollar_amount(cents)
260
+ "%0.2f" % (cents / 100.0)
261
+ end
262
+
263
+ def handle_non_iso_country_names(country)
264
+ NON_ISO_COUNTRY_NAMES[country.to_s] || country
265
+ end
266
+ end
267
+ end
268
+ end