active_shipping 0.1.4 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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