benhutton-active_shipping 0.9.13

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.
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,331 @@
1
+ # FedEx module by Jimmy Baker
2
+ # http://github.com/jimmyebaker
3
+
4
+ module ActiveMerchant
5
+ module Shipping
6
+
7
+ # :key is your developer API key
8
+ # :password is your API password
9
+ # :account is your FedEx account number
10
+ # :login is your meter number
11
+ class FedEx < Carrier
12
+ self.retry_safe = true
13
+
14
+ cattr_reader :name
15
+ @@name = "FedEx"
16
+
17
+ TEST_URL = 'https://gatewaybeta.fedex.com:443/xml'
18
+ LIVE_URL = 'https://gateway.fedex.com:443/xml'
19
+
20
+ CarrierCodes = {
21
+ "fedex_ground" => "FDXG",
22
+ "fedex_express" => "FDXE"
23
+ }
24
+
25
+ ServiceTypes = {
26
+ "PRIORITY_OVERNIGHT" => "FedEx Priority Overnight",
27
+ "PRIORITY_OVERNIGHT_SATURDAY_DELIVERY" => "FedEx Priority Overnight Saturday Delivery",
28
+ "FEDEX_2_DAY" => "FedEx 2 Day",
29
+ "FEDEX_2_DAY_SATURDAY_DELIVERY" => "FedEx 2 Day Saturday Delivery",
30
+ "STANDARD_OVERNIGHT" => "FedEx Standard Overnight",
31
+ "FIRST_OVERNIGHT" => "FedEx First Overnight",
32
+ "FIRST_OVERNIGHT_SATURDAY_DELIVERY" => "FedEx First Overnight Saturday Delivery",
33
+ "FEDEX_EXPRESS_SAVER" => "FedEx Express Saver",
34
+ "FEDEX_1_DAY_FREIGHT" => "FedEx 1 Day Freight",
35
+ "FEDEX_1_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 1 Day Freight Saturday Delivery",
36
+ "FEDEX_2_DAY_FREIGHT" => "FedEx 2 Day Freight",
37
+ "FEDEX_2_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 2 Day Freight Saturday Delivery",
38
+ "FEDEX_3_DAY_FREIGHT" => "FedEx 3 Day Freight",
39
+ "FEDEX_3_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 3 Day Freight Saturday Delivery",
40
+ "INTERNATIONAL_PRIORITY" => "FedEx International Priority",
41
+ "INTERNATIONAL_PRIORITY_SATURDAY_DELIVERY" => "FedEx International Priority Saturday Delivery",
42
+ "INTERNATIONAL_ECONOMY" => "FedEx International Economy",
43
+ "INTERNATIONAL_FIRST" => "FedEx International First",
44
+ "INTERNATIONAL_PRIORITY_FREIGHT" => "FedEx International Priority Freight",
45
+ "INTERNATIONAL_ECONOMY_FREIGHT" => "FedEx International Economy Freight",
46
+ "GROUND_HOME_DELIVERY" => "FedEx Ground Home Delivery",
47
+ "FEDEX_GROUND" => "FedEx Ground",
48
+ "INTERNATIONAL_GROUND" => "FedEx International Ground"
49
+ }
50
+
51
+ PackageTypes = {
52
+ "fedex_envelope" => "FEDEX_ENVELOPE",
53
+ "fedex_pak" => "FEDEX_PAK",
54
+ "fedex_box" => "FEDEX_BOX",
55
+ "fedex_tube" => "FEDEX_TUBE",
56
+ "fedex_10_kg_box" => "FEDEX_10KG_BOX",
57
+ "fedex_25_kg_box" => "FEDEX_25KG_BOX",
58
+ "your_packaging" => "YOUR_PACKAGING"
59
+ }
60
+
61
+ DropoffTypes = {
62
+ 'regular_pickup' => 'REGULAR_PICKUP',
63
+ 'request_courier' => 'REQUEST_COURIER',
64
+ 'dropbox' => 'DROP_BOX',
65
+ 'business_service_center' => 'BUSINESS_SERVICE_CENTER',
66
+ 'station' => 'STATION'
67
+ }
68
+
69
+ PaymentTypes = {
70
+ 'sender' => 'SENDER',
71
+ 'recipient' => 'RECIPIENT',
72
+ 'third_party' => 'THIRDPARTY',
73
+ 'collect' => 'COLLECT'
74
+ }
75
+
76
+ PackageIdentifierTypes = {
77
+ 'tracking_number' => 'TRACKING_NUMBER_OR_DOORTAG',
78
+ 'door_tag' => 'TRACKING_NUMBER_OR_DOORTAG',
79
+ 'rma' => 'RMA',
80
+ 'ground_shipment_id' => 'GROUND_SHIPMENT_ID',
81
+ 'ground_invoice_number' => 'GROUND_INVOICE_NUMBER',
82
+ 'ground_customer_reference' => 'GROUND_CUSTOMER_REFERENCE',
83
+ 'ground_po' => 'GROUND_PO',
84
+ 'express_reference' => 'EXPRESS_REFERENCE',
85
+ 'express_mps_master' => 'EXPRESS_MPS_MASTER'
86
+ }
87
+
88
+ def self.service_name_for_code(service_code)
89
+ ServiceTypes[service_code] || begin
90
+ name = service_code.downcase.split('_').collect{|word| word.capitalize }.join(' ')
91
+ "FedEx #{name.sub(/Fedex /, '')}"
92
+ end
93
+ end
94
+
95
+ def requirements
96
+ [:key, :password, :account, :login]
97
+ end
98
+
99
+ def find_rates(origin, destination, packages, options = {})
100
+ options = @options.update(options)
101
+ packages = Array(packages)
102
+
103
+ rate_request = build_rate_request(origin, destination, packages, options)
104
+
105
+ response = commit(save_request(rate_request), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
106
+
107
+ parse_rate_response(origin, destination, packages, response, options)
108
+ end
109
+
110
+ def find_tracking_info(tracking_number, options={})
111
+ options = @options.update(options)
112
+
113
+ tracking_request = build_tracking_request(tracking_number, options)
114
+ response = commit(save_request(tracking_request), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
115
+ parse_tracking_response(response, options)
116
+ end
117
+
118
+ protected
119
+ def build_rate_request(origin, destination, packages, options={})
120
+ imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))
121
+
122
+ xml_request = XmlNode.new('RateRequest', 'xmlns' => 'http://fedex.com/ws/rate/v6') do |root_node|
123
+ root_node << build_request_header
124
+
125
+ # Version
126
+ root_node << XmlNode.new('Version') do |version_node|
127
+ version_node << XmlNode.new('ServiceId', 'crs')
128
+ version_node << XmlNode.new('Major', '6')
129
+ version_node << XmlNode.new('Intermediate', '0')
130
+ version_node << XmlNode.new('Minor', '0')
131
+ end
132
+
133
+ # Returns delivery dates
134
+ root_node << XmlNode.new('ReturnTransitAndCommit', true)
135
+ # Returns saturday delivery shipping options when available
136
+ root_node << XmlNode.new('VariableOptions', 'SATURDAY_DELIVERY')
137
+
138
+ root_node << XmlNode.new('RequestedShipment') do |rs|
139
+ rs << XmlNode.new('ShipTimestamp', Time.now)
140
+ rs << XmlNode.new('DropoffType', options[:dropoff_type] || 'REGULAR_PICKUP')
141
+ rs << XmlNode.new('PackagingType', options[:packaging_type] || 'YOUR_PACKAGING')
142
+
143
+ rs << build_location_node('Shipper', (options[:shipper] || origin))
144
+ rs << build_location_node('Recipient', destination)
145
+ if options[:shipper] and options[:shipper] != origin
146
+ rs << build_location_node('Origin', origin)
147
+ end
148
+
149
+ rs << XmlNode.new('RateRequestTypes', 'ACCOUNT')
150
+ rs << XmlNode.new('PackageCount', packages.size)
151
+ packages.each do |pkg|
152
+ rs << XmlNode.new('RequestedPackages') do |rps|
153
+ rps << XmlNode.new('Weight') do |tw|
154
+ tw << XmlNode.new('Units', imperial ? 'LB' : 'KG')
155
+ tw << XmlNode.new('Value', [((imperial ? pkg.lbs : pkg.kgs).to_f*1000).round/1000.0, 0.1].max)
156
+ end
157
+ rps << XmlNode.new('Dimensions') do |dimensions|
158
+ [:length,:width,:height].each do |axis|
159
+ value = ((imperial ? pkg.inches(axis) : pkg.cm(axis)).to_f*1000).round/1000.0 # 3 decimals
160
+ dimensions << XmlNode.new(axis.to_s.capitalize, value.ceil)
161
+ end
162
+ dimensions << XmlNode.new('Units', imperial ? 'IN' : 'CM')
163
+ end
164
+ end
165
+ end
166
+
167
+ end
168
+ end
169
+ xml_request.to_s
170
+ end
171
+
172
+ def build_tracking_request(tracking_number, options={})
173
+ xml_request = XmlNode.new('TrackRequest', 'xmlns' => 'http://fedex.com/ws/track/v3') do |root_node|
174
+ root_node << build_request_header
175
+
176
+ # Version
177
+ root_node << XmlNode.new('Version') do |version_node|
178
+ version_node << XmlNode.new('ServiceId', 'trck')
179
+ version_node << XmlNode.new('Major', '3')
180
+ version_node << XmlNode.new('Intermediate', '0')
181
+ version_node << XmlNode.new('Minor', '0')
182
+ end
183
+
184
+ root_node << XmlNode.new('PackageIdentifier') do |package_node|
185
+ package_node << XmlNode.new('Value', tracking_number)
186
+ package_node << XmlNode.new('Type', PackageIdentifierTypes[options['package_identifier_type'] || 'tracking_number'])
187
+ end
188
+
189
+ root_node << XmlNode.new('ShipDateRangeBegin', options['ship_date_range_begin']) if options['ship_date_range_begin']
190
+ root_node << XmlNode.new('ShipDateRangeEnd', options['ship_date_range_end']) if options['ship_date_range_end']
191
+ root_node << XmlNode.new('IncludeDetailedScans', 1)
192
+ end
193
+ xml_request.to_s
194
+ end
195
+
196
+ def build_request_header
197
+ web_authentication_detail = XmlNode.new('WebAuthenticationDetail') do |wad|
198
+ wad << XmlNode.new('UserCredential') do |uc|
199
+ uc << XmlNode.new('Key', @options[:key])
200
+ uc << XmlNode.new('Password', @options[:password])
201
+ end
202
+ end
203
+
204
+ client_detail = XmlNode.new('ClientDetail') do |cd|
205
+ cd << XmlNode.new('AccountNumber', @options[:account])
206
+ cd << XmlNode.new('MeterNumber', @options[:login])
207
+ end
208
+
209
+ trasaction_detail = XmlNode.new('TransactionDetail') do |td|
210
+ td << XmlNode.new('CustomerTransactionId', 'ActiveShipping') # TODO: Need to do something better with this..
211
+ end
212
+
213
+ [web_authentication_detail, client_detail, trasaction_detail]
214
+ end
215
+
216
+ def build_location_node(name, location)
217
+ location_node = XmlNode.new(name) do |xml_node|
218
+ xml_node << XmlNode.new('Address') do |address_node|
219
+ address_node << XmlNode.new('PostalCode', location.postal_code)
220
+ address_node << XmlNode.new("CountryCode", location.country_code(:alpha2))
221
+ end
222
+ end
223
+ end
224
+
225
+ def parse_rate_response(origin, destination, packages, response, options)
226
+ rate_estimates = []
227
+ success, message = nil
228
+
229
+ xml = REXML::Document.new(response)
230
+ root_node = xml.elements['RateReply']
231
+
232
+ success = response_success?(xml)
233
+ message = response_message(xml)
234
+
235
+ root_node.elements.each('RateReplyDetails') do |rated_shipment|
236
+ service_code = rated_shipment.get_text('ServiceType').to_s
237
+ is_saturday_delivery = rated_shipment.get_text('AppliedOptions').to_s == 'SATURDAY_DELIVERY'
238
+ service_type = is_saturday_delivery ? "#{service_code}_SATURDAY_DELIVERY" : service_code
239
+
240
+ currency = handle_uk_currency(rated_shipment.get_text('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Currency').to_s)
241
+ rate_estimates << RateEstimate.new(origin, destination, @@name,
242
+ self.class.service_name_for_code(service_type),
243
+ :service_code => service_code,
244
+ :total_price => rated_shipment.get_text('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Amount').to_s.to_f,
245
+ :currency => currency,
246
+ :packages => packages,
247
+ :delivery_date => rated_shipment.get_text('DeliveryTimestamp').to_s)
248
+ end
249
+
250
+ if rate_estimates.empty?
251
+ success = false
252
+ message = "No shipping rates could be found for the destination address" if message.blank?
253
+ end
254
+
255
+ RateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request, :log_xml => options[:log_xml])
256
+ end
257
+
258
+ def parse_tracking_response(response, options)
259
+ xml = REXML::Document.new(response)
260
+ root_node = xml.elements['TrackReply']
261
+
262
+ success = response_success?(xml)
263
+ message = response_message(xml)
264
+
265
+ if success
266
+ tracking_number, origin, destination = nil
267
+ shipment_events = []
268
+
269
+ tracking_details = root_node.elements['TrackDetails']
270
+ tracking_number = tracking_details.get_text('TrackingNumber').to_s
271
+
272
+ destination_node = tracking_details.elements['DestinationAddress']
273
+ destination = Location.new(
274
+ :country => destination_node.get_text('CountryCode').to_s,
275
+ :province => destination_node.get_text('StateOrProvinceCode').to_s,
276
+ :city => destination_node.get_text('City').to_s
277
+ )
278
+
279
+ tracking_details.elements.each('Events') do |event|
280
+ address = event.elements['Address']
281
+
282
+ city = address.get_text('City').to_s
283
+ state = address.get_text('StateOrProvinceCode').to_s
284
+ zip_code = address.get_text('PostalCode').to_s
285
+ country = address.get_text('CountryCode').to_s
286
+ next if country.blank?
287
+
288
+ location = Location.new(:city => city, :state => state, :postal_code => zip_code, :country => country)
289
+ description = event.get_text('EventDescription').to_s
290
+
291
+ # for now, just assume UTC, even though it probably isn't
292
+ time = Time.parse("#{event.get_text('Timestamp').to_s}")
293
+ zoneless_time = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec)
294
+
295
+ shipment_events << ShipmentEvent.new(description, zoneless_time, location)
296
+ end
297
+ shipment_events = shipment_events.sort_by(&:time)
298
+ end
299
+
300
+ TrackingResponse.new(success, message, Hash.from_xml(response),
301
+ :xml => response,
302
+ :request => last_request,
303
+ :shipment_events => shipment_events,
304
+ :destination => destination,
305
+ :tracking_number => tracking_number
306
+ )
307
+ end
308
+
309
+ def response_status_node(document)
310
+ document.elements['/*/Notifications/']
311
+ end
312
+
313
+ def response_success?(document)
314
+ %w{SUCCESS WARNING NOTE}.include? response_status_node(document).get_text('Severity').to_s
315
+ end
316
+
317
+ def response_message(document)
318
+ response_node = response_status_node(document)
319
+ "#{response_status_node(document).get_text('Severity').to_s} - #{response_node.get_text('Code').to_s}: #{response_node.get_text('Message').to_s}"
320
+ end
321
+
322
+ def commit(request, test = false)
323
+ ssl_post(test ? TEST_URL : LIVE_URL, request.gsub("\n",''))
324
+ end
325
+
326
+ def handle_uk_currency(currency)
327
+ currency =~ /UKL/i ? 'GBP' : currency
328
+ end
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,165 @@
1
+ require 'builder'
2
+
3
+ module ActiveMerchant
4
+ module Shipping
5
+ class Kunaki < Carrier
6
+ self.retry_safe = true
7
+
8
+ cattr_reader :name
9
+ @@name = "Kunaki"
10
+
11
+ URL = 'https://Kunaki.com/XMLService.ASP'
12
+
13
+ CARRIERS = [ "UPS", "USPS", "FedEx", "Royal Mail", "Parcelforce", "Pharos", "Eurotrux", "Canada Post", "DHL" ]
14
+
15
+ COUNTRIES = {
16
+ 'AR' => 'Argentina',
17
+ 'AU' => 'Australia',
18
+ 'AT' => 'Austria',
19
+ 'BE' => 'Belgium',
20
+ 'BR' => 'Brazil',
21
+ 'BG' => 'Bulgaria',
22
+ 'CA' => 'Canada',
23
+ 'CN' => 'China',
24
+ 'CY' => 'Cyprus',
25
+ 'CZ' => 'Czech Republic',
26
+ 'DK' => 'Denmark',
27
+ 'EE' => 'Estonia',
28
+ 'FI' => 'Finland',
29
+ 'FR' => 'France',
30
+ 'DE' => 'Germany',
31
+ 'GI' => 'Gibraltar',
32
+ 'GR' => 'Greece',
33
+ 'GL' => 'Greenland',
34
+ 'HK' => 'Hong Kong',
35
+ 'HU' => 'Hungary',
36
+ 'IS' => 'Iceland',
37
+ 'IE' => 'Ireland',
38
+ 'IL' => 'Israel',
39
+ 'IT' => 'Italy',
40
+ 'JP' => 'Japan',
41
+ 'LV' => 'Latvia',
42
+ 'LI' => 'Liechtenstein',
43
+ 'LT' => 'Lithuania',
44
+ 'LU' => 'Luxembourg',
45
+ 'MX' => 'Mexico',
46
+ 'NL' => 'Netherlands',
47
+ 'NZ' => 'New Zealand',
48
+ 'NO' => 'Norway',
49
+ 'PL' => 'Poland',
50
+ 'PT' => 'Portugal',
51
+ 'RO' => 'Romania',
52
+ 'RU' => 'Russia',
53
+ 'SG' => 'Singapore',
54
+ 'SK' => 'Slovakia',
55
+ 'SI' => 'Slovenia',
56
+ 'ES' => 'Spain',
57
+ 'SE' => 'Sweden',
58
+ 'CH' => 'Switzerland',
59
+ 'TW' => 'Taiwan',
60
+ 'TR' => 'Turkey',
61
+ 'UA' => 'Ukraine',
62
+ 'GB' => 'United Kingdom',
63
+ 'US' => 'United States',
64
+ 'VA' => 'Vatican City',
65
+ 'RS' => 'Yugoslavia',
66
+ 'ME' => 'Yugoslavia'
67
+ }
68
+
69
+ def find_rates(origin, destination, packages, options = {})
70
+ requires!(options, :items)
71
+ commit(origin, destination, options)
72
+ end
73
+
74
+ def valid_credentials?
75
+ true
76
+ end
77
+
78
+ private
79
+ def build_request(destination, options)
80
+ xml = Builder::XmlMarkup.new
81
+ xml.instruct!
82
+ xml.tag! 'ShippingOptions' do
83
+ xml.tag! 'AddressInfo' do
84
+ xml.tag! 'Country', COUNTRIES[destination.country_code]
85
+
86
+ state = ['US', 'CA'].include?(destination.country_code.to_s) ? destination.state : ''
87
+
88
+ xml.tag! 'State_Province', state
89
+ xml.tag! 'PostalCode', destination.zip
90
+ end
91
+
92
+ options[:items].each do |item|
93
+ xml.tag! 'Product' do
94
+ xml.tag! 'ProductId', item[:sku]
95
+ xml.tag! 'Quantity', item[:quantity]
96
+ end
97
+ end
98
+ end
99
+ xml.target!
100
+ end
101
+
102
+ def commit(origin, destination, options)
103
+ request = build_request(destination, options)
104
+
105
+ response = parse( ssl_post(URL, request, "Content-Type" => "text/xml") )
106
+
107
+ RateResponse.new(success?(response), message_from(response), response,
108
+ :rates => build_rate_estimates(response, origin, destination)
109
+ )
110
+ end
111
+
112
+ def build_rate_estimates(response, origin, destination)
113
+ response["Options"].collect do |quote|
114
+ RateEstimate.new(origin, destination, carrier_for(quote["Description"]), quote["Description"],
115
+ :total_price => quote["Price"],
116
+ :currency => "USD"
117
+ )
118
+ end
119
+ end
120
+
121
+ def carrier_for(service)
122
+ CARRIERS.dup.find{ |carrier| service.to_s =~ /^#{carrier}/i } || service.to_s.split(" ").first
123
+ end
124
+
125
+ def parse(xml)
126
+ response = {}
127
+ response["Options"] = []
128
+
129
+ document = REXML::Document.new(sanitize(xml))
130
+
131
+ response["ErrorCode"] = parse_child_text(document.root, "ErrorCode")
132
+ response["ErrorText"] = parse_child_text(document.root, "ErrorText")
133
+
134
+ document.root.elements.each("Option") do |e|
135
+ rate = {}
136
+ rate["Description"] = parse_child_text(e, "Description")
137
+ rate["Price"] = parse_child_text(e, "Price")
138
+ response["Options"] << rate
139
+ end
140
+ response
141
+ end
142
+
143
+ def sanitize(response)
144
+ result = response.to_s
145
+ result.gsub!("\r\n", "")
146
+ result.gsub!(/<(\/)?(BODY|HTML)>/, '')
147
+ result
148
+ end
149
+
150
+ def parse_child_text(parent, name)
151
+ if element = parent.elements[name]
152
+ element.text
153
+ end
154
+ end
155
+
156
+ def success?(response)
157
+ response["ErrorCode"] == "0"
158
+ end
159
+
160
+ def message_from(response)
161
+ response["ErrorText"]
162
+ end
163
+ end
164
+ end
165
+ end