active_shipping 0.1.4 → 0.9.1

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 (73) hide show
  1. data/CHANGELOG +2 -0
  2. data/README.markdown +0 -3
  3. data/lib/active_merchant/common.rb +14 -0
  4. data/lib/{active_shipping/lib → active_merchant/common}/connection.rb +2 -0
  5. data/lib/{active_shipping/lib → active_merchant/common}/country.rb +0 -0
  6. data/lib/active_merchant/common/error.rb +26 -0
  7. data/lib/active_merchant/common/post_data.rb +24 -0
  8. data/lib/{active_shipping/lib → active_merchant/common}/posts_data.rb +0 -0
  9. data/lib/{active_shipping/lib → active_merchant/common}/requires_parameters.rb +0 -0
  10. data/lib/{active_shipping/lib → active_merchant/common}/utils.rb +0 -0
  11. data/lib/{active_shipping/lib → active_merchant/common}/validateable.rb +0 -0
  12. data/lib/active_shipping.rb +10 -22
  13. data/lib/active_shipping/shipping/base.rb +2 -1
  14. data/lib/active_shipping/shipping/carrier.rb +0 -5
  15. data/lib/active_shipping/shipping/carriers.rb +2 -1
  16. data/lib/active_shipping/shipping/carriers/fedex.rb +7 -191
  17. data/lib/active_shipping/shipping/carriers/kunaki.rb +165 -0
  18. data/lib/active_shipping/shipping/carriers/ups.rb +2 -1
  19. data/lib/active_shipping/shipping/carriers/usps.rb +0 -85
  20. data/lib/active_shipping/shipping/location.rb +1 -4
  21. data/lib/active_shipping/shipping/response.rb +1 -4
  22. data/lib/active_shipping/version.rb +3 -0
  23. data/lib/vendor/quantified/lib/quantified.rb +5 -3
  24. metadata +68 -102
  25. data/.gitignore +0 -8
  26. data/Rakefile +0 -51
  27. data/VERSION +0 -1
  28. data/active_shipping.gemspec +0 -152
  29. data/init.rb +0 -1
  30. data/lib/active_shipping/lib/error.rb +0 -4
  31. data/lib/active_shipping/lib/post_data.rb +0 -22
  32. data/lib/active_shipping/shipping/contact.rb +0 -18
  33. data/lib/active_shipping/shipping/label.rb +0 -31
  34. data/lib/active_shipping/shipping/location_response.rb +0 -14
  35. data/lib/active_shipping/shipping/party.rb +0 -15
  36. data/lib/active_shipping/shipping/return_label_response.rb +0 -14
  37. data/lib/active_shipping/shipping/return_shipment.rb +0 -14
  38. data/lib/active_shipping/shipping/shipment.rb +0 -73
  39. data/test/fixtures.example.yml +0 -13
  40. data/test/fixtures/xml/fedex/ottawa_to_beverly_hills_rate_request.xml +0 -67
  41. data/test/fixtures/xml/fedex/ottawa_to_beverly_hills_rate_response.xml +0 -213
  42. data/test/fixtures/xml/fedex/tracking_request.xml +0 -27
  43. data/test/fixtures/xml/fedex/tracking_response.xml +0 -153
  44. data/test/fixtures/xml/shipwire/international_rates_response.xml +0 -17
  45. data/test/fixtures/xml/shipwire/invalid_credentials_response.xml +0 -4
  46. data/test/fixtures/xml/shipwire/new_carrier_rate_response.xml +0 -18
  47. data/test/fixtures/xml/shipwire/no_rates_response.xml +0 -7
  48. data/test/fixtures/xml/shipwire/rates_response.xml +0 -36
  49. data/test/fixtures/xml/ups/example_tracking_response.xml +0 -53
  50. data/test/fixtures/xml/ups/shipment_from_tiger_direct.xml +0 -222
  51. data/test/fixtures/xml/ups/test_real_home_as_residential_destination_response.xml +0 -1
  52. data/test/fixtures/xml/usps/beverly_hills_to_ottawa_book_rate_response.xml +0 -85
  53. data/test/fixtures/xml/usps/beverly_hills_to_ottawa_book_wii_rate_response.xml +0 -168
  54. data/test/fixtures/xml/usps/beverly_hills_to_ottawa_wii_rate_response.xml +0 -85
  55. data/test/fixtures/xml/usps/example_tracking_response.xml +0 -104
  56. data/test/fixtures/xml/usps/multi_tracking_example.xml +0 -105
  57. data/test/party_factory.rb +0 -29
  58. data/test/remote/fedex_test.rb +0 -160
  59. data/test/remote/shipwire_test.rb +0 -88
  60. data/test/remote/ups_test.rb +0 -207
  61. data/test/remote/usps_test.rb +0 -184
  62. data/test/shipment_factory.rb +0 -27
  63. data/test/test_helper.rb +0 -171
  64. data/test/unit/base_test.rb +0 -18
  65. data/test/unit/carriers/fedex_test.rb +0 -78
  66. data/test/unit/carriers/shipwire_test.rb +0 -130
  67. data/test/unit/carriers/ups_test.rb +0 -81
  68. data/test/unit/carriers/usps_test.rb +0 -206
  69. data/test/unit/location_test.rb +0 -46
  70. data/test/unit/package_test.rb +0 -65
  71. data/test/unit/party_test.rb +0 -20
  72. data/test/unit/response_test.rb +0 -10
  73. data/test/unit/shipment_test.rb +0 -43
data/CHANGELOG CHANGED
@@ -1,3 +1,5 @@
1
+ * Assume test_helper is in load path when running tests [cody]
2
+ * Add support Kunaki rating service [cody]
1
3
  * Require active_support instead of activesupport to avoid deprecation warning in Rails 2.3.5 [cody]
2
4
  * Remove ftools for Rails 1.9 compatibility and remove xml logging, as logging is now included in the connection [cody]
3
5
  * Update connection code from ActiveMerchant [cody]
data/README.markdown CHANGED
@@ -38,8 +38,6 @@ You will need to get [Git][] if you don't have it. Then:
38
38
 
39
39
  Active Shipping includes an init.rb file. This means that Rails will automatically load it on startup. Check out [git-archive][] for exporting the file tree from your repository to your vendor directory.
40
40
 
41
- Gem and tarball forthcoming on rubyforge.
42
-
43
41
  [Git]:http://git.or.cz/
44
42
  [git-archive]:http://www.kernel.org/pub/software/scm/git/docs/git-archive.html
45
43
 
@@ -144,7 +142,6 @@ Gem and tarball forthcoming on rubyforge.
144
142
 
145
143
  * proper documentation
146
144
  * proper offline testing for carriers in addition to the remote tests
147
- * package into a gem
148
145
  * carrier code template generator
149
146
  * more carriers
150
147
  * integrate with ActiveMerchant
@@ -0,0 +1,14 @@
1
+ module ActiveMerchant
2
+ autoload :Connection, 'active_merchant/common/connection'
3
+ autoload :Country, 'active_merchant/common/country'
4
+ autoload :ActiveMerchantError, 'active_merchant/common/error'
5
+ autoload :ConnectionError, 'active_merchant/common/error'
6
+ autoload :RetriableConnectionError, 'active_merchant/common/error'
7
+ autoload :ResponseError, 'active_merchant/common/error'
8
+ autoload :ClientCertificateError, 'active_merchant/common/error'
9
+ autoload :PostData, 'active_merchant/common/post_data'
10
+ autoload :PostsData, 'active_merchant/common/posts_data'
11
+ autoload :RequiresParameters, 'active_merchant/common/requires_parameters'
12
+ autoload :Utils, 'active_merchant/common/utils'
13
+ autoload :Validateable, 'active_merchant/common/validateable'
14
+ end
@@ -80,6 +80,8 @@ module ActiveMerchant
80
80
  raise ConnectionError, "The remote server reset the connection"
81
81
  rescue Errno::ECONNREFUSED => e
82
82
  raise RetriableConnectionError, "The remote server refused the connection"
83
+ rescue OpenSSL::X509::CertificateError => e
84
+ raise ClientCertificateError, "The remote server did not accept the provided SSL certificate"
83
85
  rescue Timeout::Error, Errno::ETIMEDOUT => e
84
86
  raise ConnectionError, "The connection to the remote server timed out"
85
87
  end
@@ -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
@@ -23,20 +23,19 @@
23
23
 
24
24
  $:.unshift File.dirname(__FILE__)
25
25
 
26
- require 'rubygems'
27
- require 'active_support'
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
28
33
 
29
- require 'vendor/xml_node/lib/xml_node'
30
- require 'vendor/quantified/lib/quantified'
31
- require 'quantified/mass'
32
- require 'quantified/length'
34
+ autoload :XmlNode, 'vendor/xml_node/lib/xml_node'
35
+ autoload :Quantified, 'vendor/quantified/lib/quantified'
33
36
 
34
37
  require 'net/https'
35
- require 'active_shipping/lib/error'
36
- require 'active_shipping/lib/requires_parameters'
37
- require 'active_shipping/lib/connection'
38
- require 'active_shipping/lib/posts_data'
39
- require 'active_shipping/lib/country'
38
+ require 'active_merchant/common'
40
39
 
41
40
  require 'active_shipping/shipping/base'
42
41
  require 'active_shipping/shipping/response'
@@ -45,17 +44,6 @@ require 'active_shipping/shipping/tracking_response'
45
44
  require 'active_shipping/shipping/package'
46
45
  require 'active_shipping/shipping/location'
47
46
  require 'active_shipping/shipping/rate_estimate'
48
- require 'active_shipping/shipping/location_response'
49
- require 'active_shipping/shipping/return_label_response'
50
-
51
- require 'active_shipping/shipping/contact'
52
- require 'active_shipping/shipping/party'
53
- require 'active_shipping/shipping/label'
54
-
55
- require 'active_shipping/shipping/shipment'
56
- require 'active_shipping/shipping/return_shipment'
57
-
58
47
  require 'active_shipping/shipping/shipment_event'
59
48
  require 'active_shipping/shipping/carrier'
60
49
  require 'active_shipping/shipping/carriers'
61
-
@@ -5,7 +5,8 @@ module ActiveMerchant
5
5
  self.mode = :production
6
6
 
7
7
  def self.carrier(name)
8
- ActiveMerchant::Shipping::Carriers.all.find {|c| c.name.downcase == name}
8
+ ActiveMerchant::Shipping::Carriers.all.find {|c| c.name.downcase == name.to_s.downcase} ||
9
+ raise(NameError, "unknown carrier #{name}")
9
10
  end
10
11
  end
11
12
  end
@@ -45,11 +45,6 @@ module ActiveMerchant
45
45
 
46
46
  protected
47
47
 
48
- def node_string_or_nil(xml_node)
49
- text = node_text_or_nil(xml_node)
50
- text ? text.to_s : nil
51
- end
52
-
53
48
  def node_text_or_nil(xml_node)
54
49
  xml_node ? xml_node.text : nil
55
50
  end
@@ -3,13 +3,14 @@ require 'active_shipping/shipping/carriers/ups'
3
3
  require 'active_shipping/shipping/carriers/usps'
4
4
  require 'active_shipping/shipping/carriers/fedex'
5
5
  require 'active_shipping/shipping/carriers/shipwire'
6
+ require 'active_shipping/shipping/carriers/kunaki'
6
7
 
7
8
  module ActiveMerchant
8
9
  module Shipping
9
10
  module Carriers
10
11
  class <<self
11
12
  def all
12
- [BogusCarrier, UPS, USPS, FedEx, Shipwire]
13
+ [BogusCarrier, UPS, USPS, FedEx, Shipwire, Kunaki]
13
14
  end
14
15
  end
15
16
  end
@@ -106,152 +106,8 @@ module ActiveMerchant
106
106
  response = commit(save_request(tracking_request), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
107
107
  parse_tracking_response(response, options)
108
108
  end
109
-
110
- def get_return_label(shipment, options = {})
111
- req = build_return_label_request(shipment)
112
- response = commit(save_request(req), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
113
-
114
- parse_return_label_response(response, shipment)
115
- end
116
-
117
- def validate_location(location, options = {})
118
- req = build_location_validation_request(location)
119
- response = commit(save_request(req), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
120
-
121
- parse_location_validation_request(response, location)
122
- end
123
-
109
+
124
110
  protected
125
- def build_location_validation_request(location)
126
- xml_request = XmlNode.new('AddressValidationRequest',
127
- 'xmlns' => 'http://fedex.com/ws/addressvalidation/v2') do |root_node|
128
-
129
- root_node << build_request_header
130
- root_node << XmlNode.new('Version') do |version_node|
131
- version_node << XmlNode.new('ServiceId', 'aval')
132
- version_node << XmlNode.new('Major', '2')
133
- version_node << XmlNode.new('Intermediate', '0')
134
- version_node << XmlNode.new('Minor', '0')
135
- end
136
-
137
- root_node << XmlNode.new('RequestTimestamp', Time.now)
138
- root_node << XmlNode.new('Options', '')
139
- root_node << XmlNode.new('AddressesToValidate') do |v|
140
- v << build_party_location_node('Address', location)
141
- end
142
- end
143
-
144
- xml_request.to_s
145
- end
146
-
147
- def parse_location_validation_request(response, location)
148
- xml = REXML::Document.new(response)
149
- success = response_success?(xml)
150
- message = response_message(xml)
151
-
152
- if success
153
- parent = xml.elements.first
154
- location.valid = parent.elements['//DeliveryPointValidation'][0] == 'CONFIRMED'
155
- location.score = parent.elements['//Score'][0].to_s.to_i
156
- else
157
- location.valid = false
158
- location.score = 0
159
- message
160
- end
161
- LocationResponse.new(success, message, Hash.from_xml(response),
162
- :xml => response,
163
- :request => last_request,
164
- :location => location
165
- )
166
- end
167
-
168
- def parse_return_label_response(response, shipment)
169
- xml = REXML::Document.new(response)
170
- success = response_success?(xml)
171
- message = response_message(xml)
172
-
173
- if success
174
- parent = xml.elements.first
175
- shipment.label.image = Base64.decode64(parent.elements['//Image'][0].to_s)
176
- shipment.tracking_number = parent.elements['//TrackingNumber'][0].to_s
177
- shipment.transit_time = parent.elements['//TransitTime'][0].to_s
178
-
179
- shipment
180
- end
181
- ReturnLabelResponse.new(success, message, Hash.from_xml(response),
182
- :xml => response,
183
- :request => last_request,
184
- :shipment => shipment
185
- )
186
- end
187
-
188
- def build_return_label_request(shipment)
189
- xml_request = XmlNode.new('ProcessShipmentRequest',
190
- 'xmlns' => 'http://fedex.com/ws/ship/v7') do |root_node|
191
- root_node << build_request_header
192
- root_node << XmlNode.new('Version') do |version_node|
193
- version_node << XmlNode.new('ServiceId', 'ship')
194
- version_node << XmlNode.new('Major', '7')
195
- version_node << XmlNode.new('Intermediate', '0')
196
- version_node << XmlNode.new('Minor', '0')
197
- end
198
-
199
- root_node << XmlNode.new('RequestedShipment') do |rs|
200
- rs << XmlNode.new('ShipTimestamp', shipment.ship_at || Time.now)
201
- rs << XmlNode.new('DropoffType', shipment.dropoff_type)
202
- rs << XmlNode.new('ServiceType', shipment.service)
203
- rs << XmlNode.new('PackagingType', shipment.packaging_type)
204
- rs << XmlNode.new('TotalWeight') do |t|
205
- t << XmlNode.new('Units', shipment.total_weight_units)
206
- t << XmlNode.new('Value', shipment.total_weight_value)
207
- end
208
- if shipment.total_insured_amount
209
- rs << XmlNode.new('TotalInsuredValue') do |ins|
210
- ins << XmlNode.new('Currency', shipment.total_insured_currency)
211
- ins << XmlNode.new('Amount', shipment.total_insured_amount)
212
- end
213
- end
214
- rs << build_party_node('Shipper', shipment.shipper)
215
- rs << build_party_node('Recipient', shipment.recipient)
216
- rs << XmlNode.new('ShippingChargesPayment') do |payment|
217
- payment << XmlNode.new('PaymentType', shipment.payment_type)
218
- payment << XmlNode.new('Payor') do |payor|
219
- payor << XmlNode.new('AccountNumber', shipment.payor_account_number)
220
- payor << XmlNode.new('CountryCode', shipment.payor_account_country.code(:alpha2))
221
- end
222
- end
223
-
224
- rs << XmlNode.new('SpecialServicesRequested') do |s|
225
- s << XmlNode.new('SpecialServiceTypes', 'RETURN_SHIPMENT')
226
- s << XmlNode.new('ReturnShipmentDetail') do |d|
227
- d << XmlNode.new('ReturnType', shipment.return_type)
228
- d << XmlNode.new('Rma') do |rma|
229
- rma << XmlNode.new('Number', shipment.rma_number)
230
- end
231
- end
232
- end
233
-
234
- rs << XmlNode.new('LabelSpecification') do |spec|
235
- spec << XmlNode.new('LabelFormatType',
236
- shipment.label.format_type || 'COMMON2D')
237
- spec << XmlNode.new('ImageType', shipment.label.image_type)
238
- spec << XmlNode.new('LabelStockType', 'PAPER_4X6')
239
- end
240
- rs << XmlNode.new('RateRequestTypes', shipment.rate_request_type)
241
- rs << XmlNode.new('PackageCount', shipment.package_count)
242
- rs << XmlNode.new('PackageDetail', 'PACKAGE_SUMMARY')
243
- (1..shipment.package_count).each do |package_index|
244
- rs << XmlNode.new('RequestedPackageLineItems') do |p|
245
- p << XmlNode.new('SequenceNumber', package_index)
246
- end
247
- end
248
- end
249
- end
250
-
251
- xml_request.to_s
252
- end
253
-
254
-
255
111
  def build_rate_request(origin, destination, packages, options={})
256
112
  imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))
257
113
 
@@ -349,47 +205,6 @@ module ActiveMerchant
349
205
  [web_authentication_detail, client_detail, trasaction_detail]
350
206
  end
351
207
 
352
- def build_party_node(name, party)
353
- XmlNode.new(name) do |xml_node|
354
- if party.account_number
355
- xml_node << XmlNode.new('AccountNumber', party.account_number)
356
- end
357
-
358
- if party.contact
359
- xml_node << XmlNode.new('Contact') do |c|
360
- c << XmlNode.new('PersonName', party.contact.name)
361
- c << XmlNode.new('Title', party.contact.title)
362
- c << XmlNode.new('CompanyName', party.contact.company_name)
363
- c << XmlNode.new('PhoneNumber', party.contact.phone_number)
364
- c << XmlNode.new('FaxNumber', party.contact.fax_number)
365
- c << XmlNode.new('EMailAddress', party.contact.email_address)
366
- end
367
- end
368
-
369
-
370
- if party.location
371
- xml_node << build_party_location_node('Address', party.location)
372
- end
373
- end
374
- end
375
-
376
- def build_party_location_node(name, location)
377
- XmlNode.new(name) do |a|
378
- if location.address1
379
- a << XmlNode.new('StreetLines', location.address1)
380
- end
381
-
382
- if location.address2
383
- a << XmlNode.new('StreetLines', location.address2)
384
- end
385
-
386
- a << XmlNode.new('City', location.city)
387
- a << XmlNode.new('StateOrProvinceCode', location.state)
388
- a << XmlNode.new('PostalCode', location.zip)
389
- a << XmlNode.new('CountryCode', location.country.code(:alpha2))
390
- end
391
- end
392
-
393
208
  def build_location_node(name, location)
394
209
  location_node = XmlNode.new(name) do |xml_node|
395
210
  xml_node << XmlNode.new('Address') do |address_node|
@@ -446,9 +261,8 @@ module ActiveMerchant
446
261
  tracking_number = tracking_details.get_text('TrackingNumber').to_s
447
262
 
448
263
  destination_node = tracking_details.elements['DestinationAddress']
449
-
450
264
  destination = Location.new(
451
- :country => destination_node.get_text('CountryCode').to_s.blank? ? 'US' : destination_node.get_text('CountryCode').to_s,
265
+ :country => destination_node.get_text('CountryCode').to_s,
452
266
  :province => destination_node.get_text('StateOrProvinceCode').to_s,
453
267
  :city => destination_node.get_text('City').to_s
454
268
  )
@@ -458,12 +272,14 @@ module ActiveMerchant
458
272
  :city => event.elements['Address'].get_text('City').to_s,
459
273
  :state => event.elements['Address'].get_text('StateOrProvinceCode').to_s,
460
274
  :postal_code => event.elements['Address'].get_text('PostalCode').to_s,
461
- :country => event.elements['Address'].get_text('CountryCode').to_s.blank? ? 'US' : destination_node.get_text('CountryCode').to_s)
275
+ :country => event.elements['Address'].get_text('CountryCode').to_s)
462
276
  description = event.get_text('EventDescription').to_s
463
277
 
464
- time = Time.parse(event.get_text('Timestamp').to_s)
278
+ # for now, just assume UTC, even though it probably isn't
279
+ time = Time.parse("#{event.get_text('Timestamp').to_s}")
280
+ zoneless_time = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec)
465
281
 
466
- shipment_events << ShipmentEvent.new(description, time, location)
282
+ shipment_events << ShipmentEvent.new(description, zoneless_time, location)
467
283
  end
468
284
  shipment_events = shipment_events.sort_by(&:time)
469
285
  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