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,372 @@
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] || "FedEx #{service_code.titleize.sub(/Fedex /, '')}"
90
+ end
91
+
92
+ def requirements
93
+ [:key, :password, :account, :login]
94
+ end
95
+
96
+ def find_rates(origin, destination, packages, options = {})
97
+ options = @options.update(options)
98
+ packages = Array(packages)
99
+
100
+ rate_request = build_rate_request(origin, destination, packages, options)
101
+
102
+ response = commit(save_request(rate_request), (options[:test] || false))
103
+
104
+ parse_rate_response(origin, destination, packages, response, options)
105
+ end
106
+
107
+ def find_tracking_info(tracking_number, options={})
108
+ options = @options.update(options)
109
+
110
+ tracking_request = build_tracking_request(tracking_number, options)
111
+ response = commit(save_request(tracking_request), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
112
+ parse_tracking_response(response, options)
113
+ end
114
+
115
+ protected
116
+
117
+ def build_rate_request(origin, destination, packages, options={})
118
+ imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))
119
+
120
+ xml_request = Nokogiri::XML::Builder.new do
121
+ RateRequest(:xmlns => 'http://fedex.com/ws/rate/v6') {
122
+ # Header Information
123
+ WebAuthenticationDetail {
124
+ UserCredential {
125
+ Key options[:key]
126
+ Password options[:password]
127
+ }
128
+ }
129
+ ClientDetail {
130
+ AccountNumber options[:account]
131
+ MeterNumber options[:login]
132
+ }
133
+ TransactionDetail {
134
+ CustomerTransactionId 'ActiveShipping'
135
+ }
136
+
137
+ # Version
138
+ Version {
139
+ ServiceId 'crs'
140
+ Major '6'
141
+ Intermediate '0'
142
+ Minor '0'
143
+ }
144
+
145
+ # Returns Delivery Dates
146
+ ReturnTransitAndCommit true
147
+ # Returns saturday delivery shipping options when available
148
+ VariableOptions 'SATURDAY_DELIVERY'
149
+
150
+ RequestedShipment {
151
+ ShipTimestamp Time.now.xmlschema
152
+ DropoffType options[:dropoff_type] || 'REGULAR_PICKUP'
153
+ PackagingType options[:packaging_type] || 'YOUR_PACKAGING'
154
+
155
+ Shipper {
156
+ location = (options[:shipper] || origin)
157
+ Address {
158
+ PostalCode location.postal_code
159
+ CountryCode location.country_code(:alpha2)
160
+ case location.address_type
161
+ when 'commercial' then Residential false
162
+ when 'residential' then Residential true
163
+ end
164
+ }
165
+ }
166
+ Recipient {
167
+ location = destination
168
+ Address {
169
+ PostalCode location.postal_code
170
+ CountryCode location.country_code(:alpha2)
171
+ case location.address_type
172
+ when 'commercial' then Residential false
173
+ when 'residential' then Residential true
174
+ end
175
+ }
176
+ }
177
+ if options[:shipper] and options[:shipper] != origin
178
+ Origin {
179
+ location = origin
180
+ Address {
181
+ PostalCode location.postal_code
182
+ CountryCode location.country_code(:alpha2)
183
+ case location.address_type
184
+ when 'commercial' then Residential false
185
+ when 'residential' then Residential true
186
+ end
187
+ }
188
+ }
189
+ end
190
+
191
+ #package
192
+ RateRequestTypes 'ACCOUNT'
193
+ PackageCount packages.size
194
+
195
+ packages.each do |pkg|
196
+ RequestedPackages {
197
+ Weight {
198
+ Units imperial ? 'LB' : 'KG'
199
+ Value [((imperial ? pkg.lbs : pkg.kgs).to_f*1000).round/1000.0, 0.1].max
200
+ }
201
+ Dimensions {
202
+ if imperial
203
+ Length(((pkg.inches(:length).to_f * 1000.0).round / 1000.0).ceil) # 3 decimals
204
+ Width(((pkg.inches(:width).to_f * 1000.0).round / 1000.0).ceil) # 3 decimals
205
+ Height(((pkg.inches(:height).to_f * 1000.0).round / 1000.0).ceil) # 3 decimals
206
+ Units 'IN'
207
+ else
208
+ Length(((pkg.cm(:length).to_f * 1000.0).round / 1000.0).ceil) # 3 decimals
209
+ Width(((pkg.cm(:width).to_f * 1000.0).round / 1000.0).ceil) # 3 decimals
210
+ Height(((pkg.cm(:height).to_f * 1000.0).round / 1000.0).ceil) # 3 decimals
211
+ Units 'CM'
212
+ end
213
+ }
214
+ }
215
+ end
216
+ }
217
+ }
218
+ end
219
+ xml_request.to_xml
220
+ end
221
+
222
+ def build_tracking_request(tracking_number, options={})
223
+ xml_request = XmlNode.new('TrackRequest', 'xmlns' => 'http://fedex.com/ws/track/v3') do |root_node|
224
+ root_node << build_request_header
225
+
226
+ # Version
227
+ root_node << XmlNode.new('Version') do |version_node|
228
+ version_node << XmlNode.new('ServiceId', 'trck')
229
+ version_node << XmlNode.new('Major', '3')
230
+ version_node << XmlNode.new('Intermediate', '0')
231
+ version_node << XmlNode.new('Minor', '0')
232
+ end
233
+
234
+ root_node << XmlNode.new('PackageIdentifier') do |package_node|
235
+ package_node << XmlNode.new('Value', tracking_number)
236
+ package_node << XmlNode.new('Type', PackageIdentifierTypes[options['package_identifier_type'] || 'tracking_number'])
237
+ end
238
+
239
+ root_node << XmlNode.new('ShipDateRangeBegin', options['ship_date_range_begin']) if options['ship_date_range_begin']
240
+ root_node << XmlNode.new('ShipDateRangeEnd', options['ship_date_range_end']) if options['ship_date_range_end']
241
+ root_node << XmlNode.new('IncludeDetailedScans', 1)
242
+ end
243
+ xml_request.to_s
244
+ end
245
+
246
+ def build_request_header
247
+ web_authentication_detail = XmlNode.new('WebAuthenticationDetail') do |wad|
248
+ wad << XmlNode.new('UserCredential') do |uc|
249
+ uc << XmlNode.new('Key', @options[:key])
250
+ uc << XmlNode.new('Password', @options[:password])
251
+ end
252
+ end
253
+
254
+ client_detail = XmlNode.new('ClientDetail') do |cd|
255
+ cd << XmlNode.new('AccountNumber', @options[:account])
256
+ cd << XmlNode.new('MeterNumber', @options[:login])
257
+ end
258
+
259
+ trasaction_detail = XmlNode.new('TransactionDetail') do |td|
260
+ td << XmlNode.new('CustomerTransactionId', 'ActiveShipping') # TODO: Need to do something better with this..
261
+ end
262
+
263
+ [web_authentication_detail, client_detail, trasaction_detail]
264
+ end
265
+
266
+ def parse_rate_response(origin, destination, packages, response, options)
267
+ rate_estimates = []
268
+ success = false
269
+ message = ''
270
+
271
+ xml = Nokogiri::XML::parse(response).remove_namespaces!()
272
+ xml.root.children.each do |node|
273
+ if node.name.eql?('Notifications')
274
+ success = %w{SUCCESS WARNING NOTE}.include? node.xpath('Severity').text
275
+ message = "#{node.xpath('Severity').text} - #{node.xpath('Code').text}: #{node.xpath('Message').text}"
276
+ elsif node.name.eql?('RateReplyDetails')
277
+ service_code = node.xpath('ServiceType').text
278
+ is_saturday_delivery = node.xpath('AppliedOptions').text.eql?('SATURDAY_DELIVERY')
279
+ service_type = is_saturday_delivery ? "#{service_code}_SATURDAY_DELIVERY" : service_code
280
+
281
+ rate_type = node.xpath('ActualRateType').text
282
+ rate_estimates << RateEstimate.new(origin, destination, @@name,
283
+ self.class.service_name_for_code(service_type),
284
+ :service_code => service_code,
285
+ :total_price => node.xpath('RatedShipmentDetails/ShipmentRateDetail[RateType="' + rate_type + '"]/TotalNetCharge/Amount').text.to_f,
286
+ :currency => node.xpath('RatedShipmentDetails/ShipmentRateDetail[RateType="' + rate_type + '"]/TotalNetCharge/Currency').text,
287
+ :packages => packages,
288
+ :delivery_date => node.xpath('DeliveryTimestamp').text)
289
+ end
290
+ end
291
+
292
+ if rate_estimates.empty?
293
+ success = false
294
+ message = "No shipping rates could be found for the destination address" if message.blank?
295
+ end
296
+
297
+ RateResponse.new(success, message, {}, :rates => rate_estimates, :xml => response, :request => last_request, :log_xml => options[:log_xml])
298
+ end
299
+
300
+ def parse_tracking_response(response, options)
301
+ xml = REXML::Document.new(response)
302
+ root_node = xml.elements['TrackReply']
303
+
304
+ success = response_success?(xml)
305
+ message = response_message(xml)
306
+
307
+ if success
308
+ tracking_number, origin, destination = nil
309
+ shipment_events = []
310
+
311
+ tracking_details = root_node.elements['TrackDetails']
312
+ tracking_number = tracking_details.get_text('TrackingNumber').to_s
313
+
314
+ destination_node = tracking_details.elements['DestinationAddress']
315
+ destination = Location.new(
316
+ :country => destination_node.get_text('CountryCode').to_s,
317
+ :province => destination_node.get_text('StateOrProvinceCode').to_s,
318
+ :city => destination_node.get_text('City').to_s
319
+ )
320
+
321
+ tracking_details.elements.each('Events') do |event|
322
+ location = Location.new(
323
+ :city => event.elements['Address'].get_text('City').to_s,
324
+ :state => event.elements['Address'].get_text('StateOrProvinceCode').to_s,
325
+ :postal_code => event.elements['Address'].get_text('PostalCode').to_s,
326
+ :country => event.elements['Address'].get_text('CountryCode').to_s)
327
+ next if country.blank?
328
+
329
+ location = Location.new(:city => city, :state => state, :postal_code => zip_code, :country => country)
330
+ description = event.get_text('EventDescription').to_s
331
+
332
+ # for now, just assume UTC, even though it probably isn't
333
+ time = Time.parse("#{event.get_text('Timestamp').to_s}")
334
+ zoneless_time = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec)
335
+
336
+ shipment_events << ShipmentEvent.new(description, zoneless_time, location)
337
+ end
338
+ shipment_events = shipment_events.sort_by(&:time)
339
+ end
340
+
341
+ TrackingResponse.new(success, message, Hash.from_xml(response),
342
+ :xml => response,
343
+ :request => last_request,
344
+ :shipment_events => shipment_events,
345
+ :destination => destination,
346
+ :tracking_number => tracking_number
347
+ )
348
+ end
349
+
350
+ def response_status_node(document)
351
+ document.elements['/*/Notifications/']
352
+ end
353
+
354
+ def response_success?(document)
355
+ %w{SUCCESS WARNING NOTE}.include? response_status_node(document).get_text('Severity').to_s
356
+ end
357
+
358
+ def response_message(document)
359
+ response_node = response_status_node(document)
360
+ "#{response_status_node(document).get_text('Severity').to_s} - #{response_node.get_text('Code').to_s}: #{response_node.get_text('Message').to_s}"
361
+ end
362
+
363
+ def commit(request, test = false)
364
+ ssl_post(test ? TEST_URL : LIVE_URL, request.gsub("\n",''))
365
+ end
366
+
367
+ def handle_uk_currency(currency)
368
+ currency =~ /UKL/i ? 'GBP' : currency
369
+ end
370
+ end
371
+ end
372
+ 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