benhutton-active_shipping 0.9.13
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +38 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +142 -0
- data/lib/active_merchant/common.rb +14 -0
- data/lib/active_merchant/common/connection.rb +177 -0
- data/lib/active_merchant/common/country.rb +328 -0
- data/lib/active_merchant/common/error.rb +26 -0
- data/lib/active_merchant/common/post_data.rb +24 -0
- data/lib/active_merchant/common/posts_data.rb +63 -0
- data/lib/active_merchant/common/requires_parameters.rb +16 -0
- data/lib/active_merchant/common/utils.rb +22 -0
- data/lib/active_merchant/common/validateable.rb +76 -0
- data/lib/active_shipping.rb +49 -0
- data/lib/active_shipping/shipping/base.rb +13 -0
- data/lib/active_shipping/shipping/carrier.rb +70 -0
- data/lib/active_shipping/shipping/carriers.rb +20 -0
- data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +16 -0
- data/lib/active_shipping/shipping/carriers/canada_post.rb +268 -0
- data/lib/active_shipping/shipping/carriers/fedex.rb +331 -0
- data/lib/active_shipping/shipping/carriers/kunaki.rb +165 -0
- data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +139 -0
- data/lib/active_shipping/shipping/carriers/shipwire.rb +172 -0
- data/lib/active_shipping/shipping/carriers/ups.rb +390 -0
- data/lib/active_shipping/shipping/carriers/usps.rb +441 -0
- data/lib/active_shipping/shipping/location.rb +109 -0
- data/lib/active_shipping/shipping/package.rb +147 -0
- data/lib/active_shipping/shipping/rate_estimate.rb +54 -0
- data/lib/active_shipping/shipping/rate_response.rb +19 -0
- data/lib/active_shipping/shipping/response.rb +46 -0
- data/lib/active_shipping/shipping/shipment_event.rb +14 -0
- data/lib/active_shipping/shipping/tracking_response.rb +22 -0
- data/lib/active_shipping/version.rb +3 -0
- data/lib/certs/cacert.pem +7815 -0
- data/lib/certs/eParcel.dtd +111 -0
- data/lib/vendor/quantified/MIT-LICENSE +22 -0
- data/lib/vendor/quantified/README.markdown +49 -0
- data/lib/vendor/quantified/Rakefile +21 -0
- data/lib/vendor/quantified/init.rb +0 -0
- data/lib/vendor/quantified/lib/quantified.rb +8 -0
- data/lib/vendor/quantified/lib/quantified/attribute.rb +208 -0
- data/lib/vendor/quantified/lib/quantified/length.rb +20 -0
- data/lib/vendor/quantified/lib/quantified/mass.rb +19 -0
- data/lib/vendor/quantified/test/length_test.rb +92 -0
- data/lib/vendor/quantified/test/mass_test.rb +88 -0
- data/lib/vendor/quantified/test/test_helper.rb +2 -0
- data/lib/vendor/test_helper.rb +13 -0
- data/lib/vendor/xml_node/README +36 -0
- data/lib/vendor/xml_node/Rakefile +21 -0
- data/lib/vendor/xml_node/benchmark/bench_generation.rb +32 -0
- data/lib/vendor/xml_node/init.rb +1 -0
- data/lib/vendor/xml_node/lib/xml_node.rb +222 -0
- data/lib/vendor/xml_node/test/test_generating.rb +94 -0
- data/lib/vendor/xml_node/test/test_parsing.rb +43 -0
- 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
|