ShippingInfo 2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ require 'fedex/shipment'
2
+
3
+
4
+ # Get shipping rates trough Fedex Web Services
5
+ #
6
+ # In order to use the API you will need to apply for developer/production credentials,
7
+ # Visit {http://www.fedex.com/us/developer/ Fedex Developer Center} for more information about how to obtain your keys.
8
+ #
9
+ # ===Usage example
10
+ # #Use your own Fedex Keys
11
+ # fedex = Fedex::Shipment.new(:key => 'xxx',
12
+ # :password => 'xxxx',
13
+ # :account_number => 'xxxx',
14
+ # :meter => 'xxx',
15
+ # :mode=>['production'|'development'])
16
+ # shipper = {:name => "Sender",
17
+ # :company => "Company",
18
+ # :phone_number => "555-555-5555",
19
+ # :address => "Main Street",
20
+ # :city => "Harrison",
21
+ # :state => "AR",
22
+ # :postal_code => "72601",
23
+ # :country_code => "US" }
24
+ #
25
+ # recipient = { :name => "Recipient",
26
+ # :company => "Company",
27
+ # :phone_number => "555-555-5555",
28
+ # :address => "Main Street",
29
+ # :city => "City",
30
+ # :state => "ST",
31
+ # :postal_code => "55555",
32
+ # :country_code => "US",
33
+ # :residential => "false" }
34
+ # packages = []
35
+ # packages << { :weight => {:units => "LB", :value => 2},
36
+ # :dimensions => {:length => 10, :width => 5, :height => 4, :units => "IN" } }
37
+ # packages << { :weight => {:units => "LB", :value => 6},
38
+ # :dimensions => {:length => 5, :width => 5, :height => 4, :units => "IN" } }
39
+ # # "YOUR PACKAGING" and "REGULAR PICKUP" are the default options for all shipments but you can easily change them by passing an extra hash for # shipping_options
40
+ # shipping_options = { :packaging_type => "YOUR_PACKAGING", :drop_off_type => "REGULAR_PICKUP" }
41
+ # rate = fedex.rate({:shipper=>shipper, :recipient => recipient, :packages => packages, :service_type => "FEDEX_GROUND", :shipping_options => #shipping_options})
42
+ #
43
+ # $ <Fedex::Rate:0x1019ba5f8 @total_net_charge="34.03",
44
+ # @total_surcharges="1.93",
45
+ # @total_billing_weight="8.0 LB",
46
+ # @total_taxes="0.0",
47
+ # @rate_type="PAYOR_ACCOUNT_PACKAGE",
48
+ # @total_base_charge="32.1",
49
+ # @total_freight_discounts=nil,
50
+ # @total_net_freight="32.1",
51
+ # @rate_zone="51">
52
+ module Fedex
53
+ require 'fedex/version'
54
+ require 'ups/upsinfo'
55
+ require 'rexml/document'
56
+ #Exceptions: Fedex::RateError
57
+ class RateError < StandardError; end
58
+ end
@@ -0,0 +1,30 @@
1
+ module Fedex
2
+ class Address
3
+
4
+ attr_reader :changes, :score, :confirmed, :available, :status, :residential,
5
+ :business, :street_lines, :city, :state, :province_code,
6
+ :postal_code, :country_code
7
+
8
+ def initialize(options)
9
+ @changes = options[:changes]
10
+ @score = options[:score].to_i
11
+ @confirmed = options[:delivery_point_validation] == "CONFIRMED"
12
+ @available = options[:delivery_point_validation] != "UNAVAILABLE"
13
+
14
+ @status = options[:residential_status]
15
+ @residential = status == "RESIDENTIAL"
16
+ @business = status == "BUSINESS"
17
+
18
+ address = options[:address]
19
+
20
+ @street_lines = address[:street_lines]
21
+ @city = address[:city]
22
+ @state = address[:state_or_province_code]
23
+ @province_code = address[:state_or_province_code]
24
+ @postal_code = address[:postal_code]
25
+ @country_code = address[:country_code]
26
+
27
+ @options = options
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ require 'fedex/helpers'
2
+
3
+ module Fedex
4
+ class Credentials
5
+ include Helpers
6
+ attr_reader :key, :password, :account_number, :meter, :mode
7
+
8
+ # In order to use Fedex rates API you must first apply for a developer(and later production keys),
9
+ # Visit {http://www.fedex.com/us/developer/ Fedex Developer Center} for more information about how to obtain your keys.
10
+ # @param [String] key - Fedex web service key
11
+ # @param [String] password - Fedex password
12
+ # @param [String] account_number - Fedex account_number
13
+ # @param [String] meter - Fedex meter number
14
+ # @param [String] mode - [development/production]
15
+ #
16
+ # return a Fedex::Credentials object
17
+ def initialize(options={})
18
+ requires!(options, :key, :password, :account_number, :meter, :mode)
19
+ @key = options[:key]
20
+ @password = options[:password]
21
+ @account_number = options[:account_number]
22
+ @meter = options[:meter]
23
+ @mode = options[:mode]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ module Fedex
2
+ module Helpers
3
+
4
+ private
5
+ # String or :symbol to CamelCase
6
+ def camelize(s)
7
+ # s.to_s.split('_').map { |e| e.capitalize }.join('')
8
+ s.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
9
+ end
10
+
11
+ # Helper method to validate required fields
12
+ def requires!(hash, *params)
13
+ params.each { |param| raise RateError, "Missing Required Parameter #{param}" if hash[param].nil? }
14
+ end
15
+
16
+ def underscorize(key) #:nodoc:
17
+ key.to_s.sub(/^(v[0-9]+|ns):/, "").gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,56 @@
1
+ require 'base64'
2
+ require 'pathname'
3
+
4
+ module Fedex
5
+ class Label
6
+ attr_accessor :options, :image, :response_details
7
+
8
+ # Initialize Fedex::Label Object
9
+ # @param [Hash] options
10
+ def initialize(label_details = {})
11
+ @response_details = label_details[:process_shipment_reply]
12
+ package_details = label_details[:process_shipment_reply][:completed_shipment_detail][:completed_package_details]
13
+ @options = package_details[:label]
14
+ @options[:format] = label_details[:format]
15
+ @options[:tracking_number] = package_details[:tracking_ids][:tracking_number]
16
+ @options[:file_name] = label_details[:file_name]
17
+
18
+ @image = Base64.decode64(options[:parts][:image]) if has_image?
19
+
20
+ if file_name = @options[:file_name]
21
+ save(file_name, false)
22
+ end
23
+ end
24
+
25
+ def name
26
+ [tracking_number, format].join('.')
27
+ end
28
+
29
+ def format
30
+ options[:format]
31
+ end
32
+
33
+ def file_name
34
+ options[:file_name]
35
+ end
36
+
37
+ def tracking_number
38
+ options[:tracking_number]
39
+ end
40
+
41
+ def has_image?
42
+ options[:parts] && options[:parts][:image]
43
+ end
44
+
45
+ def save(path, append_name = true)
46
+ return unless has_image?
47
+
48
+ full_path = Pathname.new(path)
49
+ full_path = full_path.join(name) if append_name
50
+
51
+ File.open(full_path, 'wb') do|f|
52
+ f.write(@image)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,36 @@
1
+ module Fedex
2
+ # Visit {http://www.fedex.com/us/developer/ Fedex Developer Center} for a complete list of values returned from the API
3
+ #
4
+ # Rate totals are contained in the node
5
+ # response[:rate_reply][:rate_reply_details][:rated_shipment_details]
6
+ class Rate
7
+ # Initialize Fedex::Rate Object
8
+ # @param [Hash] options
9
+ #
10
+ #
11
+ # return [Fedex::Rate Object]
12
+ # @rate_type #Type used for this specific set of rate data
13
+ # @rate_zone #Indicates the rate zone used(based on origin and destination)
14
+ # @total_billing_weight #The weight used to calculate these rates
15
+ # @total_freight_discounts #The toal discounts used in the rate calculation
16
+ # @total_net_charge #The net charge after applying all discounts and surcharges
17
+ # @total_taxes #Total of the transportation-based taxes
18
+ # @total_net_freight #The freight charge minus dicounts
19
+ # @total_surcharges #The total amount of all surcharges applied to this shipment
20
+ # @total_base_charge #The total base charge
21
+ attr_accessor :rate_type, :rate_zone, :total_bilint_weight, :total_freight_discounts, :total_net_charge, :total_taxes, :total_net_freight, :total_surcharges, :total_base_charge
22
+ def initialize(options = {})
23
+ @rate_type = options[:rate_type]
24
+ @rate_zone = options[:rate_zone]
25
+ @total_billing_weight = "#{options[:total_billing_weight][:value]} #{options[:total_billing_weight][:units]}"
26
+ @total_freight_discounts = options[:total_freight_discounts]
27
+ @total_net_charge = options[:total_net_charge][:amount]
28
+ @total_taxes = options[:total_taxes][:amount]
29
+ @total_net_freight = options[:total_net_freight][:amount]
30
+ @total_surcharges = options[:total_surcharges][:amount]
31
+ @total_base_charge = options[:total_base_charge][:amount]
32
+ @total_net_fedex_charge = (options[:total_net_fe_dex_charge]||{})[:amount]
33
+ @total_rebates = (options[:total_rebates]||{})[:amount]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,92 @@
1
+ require 'fedex/request/base'
2
+ require 'fedex/address'
3
+ require 'fileutils'
4
+
5
+ module Fedex
6
+ module Request
7
+ class Address < Base
8
+ def initialize(credentials, options={})
9
+ requires!(options, :address)
10
+ @credentials = credentials
11
+ @address = options[:address]
12
+ end
13
+
14
+ def process_request
15
+ api_response = self.class.post(api_url, :body => build_xml)
16
+ puts api_response if @debug == true
17
+ response = parse_response(api_response)
18
+ if success?(response)
19
+ options = response[:address_validation_reply][:address_results][:proposed_address_details]
20
+
21
+ Fedex::Address.new(options)
22
+ else
23
+ error_message = if response[:address_validation_reply]
24
+ [response[:address_validation_reply][:notifications]].flatten.first[:message]
25
+ else
26
+ api_response["Fault"]["detail"]["fault"]["reason"]
27
+ end rescue $1
28
+ raise RateError, error_message
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # Build xml Fedex Web Service request
35
+ def build_xml
36
+ builder = Nokogiri::XML::Builder.new do |xml|
37
+ xml.AddressValidationRequest(:xmlns => "http://fedex.com/ws/addressvalidation/v2"){
38
+ add_web_authentication_detail(xml)
39
+ add_client_detail(xml)
40
+ add_version(xml)
41
+ add_request_timestamp(xml)
42
+ add_address_validation_options(xml)
43
+ add_address_to_validate(xml)
44
+ }
45
+ end
46
+ builder.doc.root.to_xml
47
+ end
48
+
49
+ def add_request_timestamp(xml)
50
+ timestamp = Time.now
51
+
52
+ # Calculate current timezone offset manually.
53
+ # Ruby <= 1.9.2 does not support this in Time#strftime
54
+ #
55
+ utc_offest = "#{timestamp.gmt_offset < 0 ? "-" : "+"}%02d:%02d" %
56
+ (timestamp.gmt_offset / 60).abs.divmod(60)
57
+ timestamp = timestamp.strftime("%Y-%m-%dT%H:%M:%S") + utc_offest
58
+
59
+ xml.RequestTimestamp timestamp
60
+ end
61
+
62
+ def add_address_validation_options(xml)
63
+ xml.Options{
64
+ xml.CheckResidentialStatus true
65
+ }
66
+ end
67
+
68
+ def add_address_to_validate(xml)
69
+ xml.AddressesToValidate{
70
+ xml.Address{
71
+ xml.StreetLines @address[:street]
72
+ xml.City @address[:city]
73
+ xml.StateOrProvinceCode @address[:state]
74
+ xml.PostalCode @address[:postal_code]
75
+ xml.CountryCode @address[:country]
76
+ }
77
+ }
78
+ end
79
+
80
+ def service
81
+ { :id => 'aval', :version => 2 }
82
+ end
83
+
84
+ # Successful request
85
+ def success?(response)
86
+ response[:address_validation_reply] &&
87
+ %w{SUCCESS WARNING NOTE}.include?(response[:address_validation_reply][:highest_severity])
88
+ end
89
+
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,309 @@
1
+ require 'httparty'
2
+ require 'nokogiri'
3
+ require 'fedex/helpers'
4
+ require 'fedex/rate'
5
+
6
+ module Fedex
7
+ module Request
8
+ class Base
9
+ include Helpers
10
+ include HTTParty
11
+ format :xml
12
+ # If true the rate method will return the complete response from the Fedex Web Service
13
+ attr_accessor :debug
14
+ # Fedex Text URL
15
+ TEST_URL = "https://gatewaybeta.fedex.com:443/xml/"
16
+
17
+ # Fedex Production URL
18
+ PRODUCTION_URL = "https://gateway.fedex.com:443/xml/"
19
+
20
+ # List of available Service Types
21
+ SERVICE_TYPES = %w(EUROPE_FIRST_INTERNATIONAL_PRIORITY FEDEX_1_DAY_FREIGHT FEDEX_2_DAY FEDEX_2_DAY_AM FEDEX_2_DAY_FREIGHT FEDEX_3_DAY_FREIGHT FEDEX_EXPRESS_SAVER FEDEX_FIRST_FREIGHT FEDEX_FREIGHT_ECONOMY FEDEX_FREIGHT_PRIORITY FEDEX_GROUND FIRST_OVERNIGHT GROUND_HOME_DELIVERY INTERNATIONAL_ECONOMY INTERNATIONAL_ECONOMY_FREIGHT INTERNATIONAL_FIRST INTERNATIONAL_PRIORITY INTERNATIONAL_PRIORITY_FREIGHT PRIORITY_OVERNIGHT SMART_POST STANDARD_OVERNIGHT)
22
+
23
+ # List of available Packaging Type
24
+ PACKAGING_TYPES = %w(FEDEX_10KG_BOX FEDEX_25KG_BOX FEDEX_BOX FEDEX_ENVELOPE FEDEX_PAK FEDEX_TUBE YOUR_PACKAGING)
25
+
26
+ # List of available DropOffTypes
27
+ DROP_OFF_TYPES = %w(BUSINESS_SERVICE_CENTER DROP_BOX REGULAR_PICKUP REQUEST_COURIER STATION)
28
+
29
+ # Clearance Brokerage Type
30
+ CLEARANCE_BROKERAGE_TYPE = %w(BROKER_INCLUSIVE BROKER_INCLUSIVE_NON_RESIDENT_IMPORTER BROKER_SELECT BROKER_SELECT_NON_RESIDENT_IMPORTER BROKER_UNASSIGNED)
31
+
32
+ # Recipient Custom ID Type
33
+ RECIPIENT_CUSTOM_ID_TYPE = %w(COMPANY INDIVIDUAL PASSPORT)
34
+
35
+ # In order to use Fedex rates API you must first apply for a developer(and later production keys),
36
+ # Visit {http://www.fedex.com/us/developer/ Fedex Developer Center} for more information about how to obtain your keys.
37
+ # @param [String] key - Fedex web service key
38
+ # @param [String] password - Fedex password
39
+ # @param [String] account_number - Fedex account_number
40
+ # @param [String] meter - Fedex meter number
41
+ # @param [String] mode - [development/production]
42
+ #
43
+ # return a Fedex::Request::Base object
44
+ def initialize(credentials, options={})
45
+ requires!(options, :shipper, :recipient, :packages, :service_type)
46
+ @credentials = credentials
47
+ @shipper, @recipient, @packages, @service_type, @customs_clearance, @debug = options[:shipper], options[:recipient], options[:packages], options[:service_type], options[:customs_clearance], options[:debug]
48
+ @debug = ENV['DEBUG'] == 'true'
49
+ @shipping_options = options[:shipping_options] ||={}
50
+ end
51
+
52
+ # Sends post request to Fedex web service and parse the response.
53
+ # Implemented by each subclass
54
+ def process_request
55
+ raise NotImplementedError, "Override process_request in subclass"
56
+ end
57
+
58
+ private
59
+ # Add web authentication detail information(key and password) to xml request
60
+ def add_web_authentication_detail(xml)
61
+ xml.WebAuthenticationDetail{
62
+ xml.UserCredential{
63
+ xml.Key @credentials.key
64
+ xml.Password @credentials.password
65
+ }
66
+ }
67
+ end
68
+
69
+ # Add Client Detail information(account_number and meter_number) to xml request
70
+ def add_client_detail(xml)
71
+ xml.ClientDetail{
72
+ xml.AccountNumber @credentials.account_number
73
+ xml.MeterNumber @credentials.meter
74
+ xml.Localization{
75
+ xml.LanguageCode 'en' # English
76
+ xml.LocaleCode 'us' # United States
77
+ }
78
+ }
79
+ end
80
+
81
+ # Add Version to xml request, using the latest version V10 Sept/2011
82
+ def add_version(xml)
83
+ xml.Version{
84
+ xml.ServiceId service[:id]
85
+ xml.Major service[:version]
86
+ xml.Intermediate 0
87
+ xml.Minor 0
88
+ }
89
+ end
90
+
91
+ # Add information for shipments
92
+ def add_requested_shipment(xml)
93
+ xml.RequestedShipment{
94
+ xml.DropoffType @shipping_options[:drop_off_type] ||= "REGULAR_PICKUP"
95
+ xml.ServiceType service_type
96
+ xml.PackagingType @shipping_options[:packaging_type] ||= "YOUR_PACKAGING"
97
+ add_shipper(xml)
98
+ add_recipient(xml)
99
+ add_shipping_charges_payment(xml)
100
+ add_customs_clearance(xml) if @customs_clearance
101
+ xml.RateRequestTypes "ACCOUNT"
102
+ add_packages(xml)
103
+ }
104
+ end
105
+
106
+ # Add shipper to xml request
107
+ def add_shipper(xml)
108
+ xml.Shipper{
109
+ xml.Contact{
110
+ xml.PersonName @shipper[:name]
111
+ xml.CompanyName @shipper[:company]
112
+ xml.PhoneNumber @shipper[:phone_number]
113
+ }
114
+ xml.Address {
115
+ Array(@shipper[:address]).take(2).each do |address_line|
116
+ xml.StreetLines address_line
117
+ end
118
+ xml.City @shipper[:city]
119
+ xml.StateOrProvinceCode @shipper[:state]
120
+ xml.PostalCode @shipper[:postal_code]
121
+ xml.CountryCode @shipper[:country_code]
122
+ }
123
+ }
124
+ end
125
+
126
+ # Add recipient to xml request
127
+ def add_recipient(xml)
128
+ xml.Recipient{
129
+ xml.Contact{
130
+ xml.PersonName @recipient[:name]
131
+ xml.CompanyName @recipient[:company]
132
+ xml.PhoneNumber @recipient[:phone_number]
133
+ }
134
+ xml.Address {
135
+ Array(@recipient[:address]).take(2).each do |address_line|
136
+ xml.StreetLines address_line
137
+ end
138
+ xml.City @recipient[:city]
139
+ xml.StateOrProvinceCode @recipient[:state]
140
+ xml.PostalCode @recipient[:postal_code]
141
+ xml.CountryCode @recipient[:country_code]
142
+ xml.Residential @recipient[:residential]
143
+ }
144
+ }
145
+ end
146
+
147
+ # Add shipping charges to xml request
148
+ def add_shipping_charges_payment(xml)
149
+ xml.ShippingChargesPayment{
150
+ xml.PaymentType "SENDER"
151
+ xml.Payor{
152
+ xml.AccountNumber @credentials.account_number
153
+ xml.CountryCode @shipper[:country_code]
154
+ }
155
+ }
156
+ end
157
+
158
+ # Add packages to xml request
159
+ def add_packages(xml)
160
+ package_count = @packages.size
161
+ xml.PackageCount package_count
162
+ @packages.each do |package|
163
+ xml.RequestedPackageLineItems{
164
+ xml.GroupPackageCount 1
165
+ xml.Weight{
166
+ xml.Units package[:weight][:units]
167
+ xml.Value package[:weight][:value]
168
+ }
169
+ if package[:dimensions]
170
+ xml.Dimensions{
171
+ xml.Length package[:dimensions][:length]
172
+ xml.Width package[:dimensions][:width]
173
+ xml.Height package[:dimensions][:height]
174
+ xml.Units package[:dimensions][:units]
175
+ }
176
+ end
177
+ if package[:customer_refrences]
178
+ xml.CustomerReferences{
179
+ package[:customer_refrences].each do |value|
180
+ xml.CustomerReferenceType 'CUSTOMER_REFERENCE'
181
+ xml.Value value
182
+ end
183
+ }
184
+ end
185
+ if package[:special_services_requested] && package[:special_services_requested][:special_service_types]
186
+ xml.SpecialServicesRequested{
187
+ if package[:special_services_requested][:special_service_types].is_a? Array
188
+ package[:special_services_requested][:special_service_types].each do |type|
189
+ xml.SpecialServiceTypes type
190
+ end
191
+ else
192
+ xml.SpecialServiceTypes package[:special_services_requested][:special_service_types]
193
+ end
194
+ # Handle COD Options
195
+ if package[:special_services_requested][:cod_detail]
196
+ xml.CodDetail{
197
+ xml.CodCollectionAmount{
198
+ xml.Currency package[:special_services_requested][:cod_detail][:cod_collection_amount][:currency]
199
+ xml.Amount package[:special_services_requested][:cod_detail][:cod_collection_amount][:amount]
200
+ }
201
+ if package[:special_services_requested][:cod_detail][:add_transportation_charges]
202
+ xml.AddTransportationCharges package[:special_services_requested][:cod_detail][:add_transportation_charges]
203
+ end
204
+ xml.CollectionType package[:special_services_requested][:cod_detail][:collection_type]
205
+ xml.CodRecipient {
206
+ add_shipper(xml)
207
+ }
208
+ if package[:special_services_requested][:cod_detail][:reference_indicator]
209
+ xml.ReferenceIndicator package[:special_services_requested][:cod_detail][:reference_indicator]
210
+ end
211
+ }
212
+ end
213
+ # DangerousGoodsDetail goes here
214
+ if package[:special_services_requested][:dry_ice_weight]
215
+ xml.DryIceWeight{
216
+ xml.Units package[:special_services_requested][:dry_ice_weight][:units]
217
+ xml.Value package[:special_services_requested][:dry_ice_weight][:value]
218
+ }
219
+ end
220
+ if package[:special_services_requested][:signature_option_detail]
221
+ xml.SignatureOptionDetail{
222
+ xml.OptionType package[:special_services_requested][:signature_option_detail][:signature_option_type]
223
+ }
224
+ end
225
+ if package[:special_services_requested][:priority_alert_detail]
226
+ xml.PriorityAlertDetail package[:special_services_requested][:priority_alert_detail]
227
+ end
228
+ }
229
+ end
230
+ }
231
+ end
232
+ end
233
+
234
+ # Add customs clearance(for international shipments)
235
+ def add_customs_clearance(xml)
236
+ xml.CustomsClearanceDetail{
237
+ hash_to_xml(xml, @customs_clearance)
238
+ }
239
+ end
240
+
241
+ # Fedex Web Service Api
242
+ def api_url
243
+ @credentials.mode == "production" ? PRODUCTION_URL : TEST_URL
244
+ end
245
+
246
+ # Build xml Fedex Web Service request
247
+ # Implemented by each subclass
248
+ def build_xml
249
+ raise NotImplementedError, "Override build_xml in subclass"
250
+ end
251
+
252
+ # Build xml nodes dynamically from the hash keys and values
253
+ def hash_to_xml(xml, hash)
254
+ hash.each do |key, value|
255
+ element = camelize(key)
256
+ if value.is_a?(Hash)
257
+ xml.send element do |x|
258
+ hash_to_xml(x, value)
259
+ end
260
+ elsif value.is_a?(Array)
261
+ value.each do |v|
262
+ xml.send element do |x|
263
+ hash_to_xml(x, v)
264
+ end
265
+ end
266
+ else
267
+ xml.send element, value
268
+ end
269
+ end
270
+ end
271
+
272
+ # Parse response, convert keys to underscore symbols
273
+ def parse_response(response)
274
+ response = sanitize_response_keys(response)
275
+ end
276
+
277
+ # Recursively sanitizes the response object by cleaning up any hash keys.
278
+ def sanitize_response_keys(response)
279
+ if response.is_a?(Hash)
280
+ response.inject({}) { |result, (key, value)| result[underscorize(key).to_sym] = sanitize_response_keys(value); result }
281
+ elsif response.is_a?(Array)
282
+ response.collect { |result| sanitize_response_keys(result) }
283
+ else
284
+ response
285
+ end
286
+ end
287
+
288
+ def service
289
+ raise NotImplementedError,
290
+ "Override service in subclass: {:id => 'service', :version => 1}"
291
+ end
292
+
293
+ # Use GROUND_HOME_DELIVERY for shipments going to a residential address within the US.
294
+ def service_type
295
+ if @recipient[:residential].to_s =~ /true/i and @service_type =~ /GROUND/i and @recipient[:country_code] =~ /US/i
296
+ "GROUND_HOME_DELIVERY"
297
+ else
298
+ @service_type
299
+ end
300
+ end
301
+
302
+ # Successful request
303
+ def success?(response)
304
+ (!response[:rate_reply].nil? and %w{SUCCESS WARNING NOTE}.include? response[:rate_reply][:highest_severity])
305
+ end
306
+
307
+ end
308
+ end
309
+ end