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,139 @@
|
|
1
|
+
module ActiveMerchant
|
2
|
+
module Shipping
|
3
|
+
class NewZealandPost < Carrier
|
4
|
+
|
5
|
+
# class NewZealandPostRateResponse < RateResponse
|
6
|
+
# end
|
7
|
+
|
8
|
+
cattr_reader :name
|
9
|
+
@@name = "New Zealand Post"
|
10
|
+
|
11
|
+
URL = "http://workshop.nzpost.co.nz/api/v1/rate.xml"
|
12
|
+
|
13
|
+
# Override to return required keys in options hash for initialize method.
|
14
|
+
def requirements
|
15
|
+
[:key]
|
16
|
+
end
|
17
|
+
|
18
|
+
# Override with whatever you need to get the rates
|
19
|
+
def find_rates(origin, destination, packages, options = {})
|
20
|
+
packages = Array(packages)
|
21
|
+
rate_responses = []
|
22
|
+
packages.each do |package|
|
23
|
+
if package.tube?
|
24
|
+
request_hash = build_tube_request_params(origin, destination, package, options)
|
25
|
+
else
|
26
|
+
request_hash = build_rectangular_request_params(origin, destination, package, options)
|
27
|
+
end
|
28
|
+
url = URL + '?' + request_hash.to_param
|
29
|
+
response = ssl_get(url)
|
30
|
+
rate_responses << parse_rate_response(origin, destination, package, response, options)
|
31
|
+
end
|
32
|
+
combine_rate_responses(rate_responses, packages)
|
33
|
+
end
|
34
|
+
|
35
|
+
def maximum_weight
|
36
|
+
Mass.new(20, :kilograms)
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
# Override in subclasses for non-U.S.-based carriers.
|
42
|
+
def self.default_location
|
43
|
+
Location.new(:postal_code => '6011')
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def build_rectangular_request_params(origin, destination, package, options = {})
|
49
|
+
params = {
|
50
|
+
:postcode_src => origin.postal_code,
|
51
|
+
:postcode_dest => destination.postal_code,
|
52
|
+
:api_key => @options[:key],
|
53
|
+
:height => "#{package.centimetres(:height) * 10}",
|
54
|
+
:thickness => "#{package.centimetres(:width) * 10}",
|
55
|
+
:length => "#{package.centimetres(:length) * 10}",
|
56
|
+
:weight =>"%.1f" % (package.weight.amount / 1000.0)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_tube_request_params(origin, destination, package, options = {})
|
61
|
+
params = {
|
62
|
+
:postcode_src => origin.postal_code,
|
63
|
+
:postcode_dest => destination.postal_code,
|
64
|
+
:api_key => @options[:key],
|
65
|
+
:diameter => "#{package.centimetres(:width) * 10}",
|
66
|
+
:length => "#{package.centimetres(:length) * 10}",
|
67
|
+
:weight => "%.1f" % (package.weight.amount / 1000.0)
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_rate_response(origin, destination, package, response, options={})
|
72
|
+
xml = REXML::Document.new(response)
|
73
|
+
if response_success?(xml)
|
74
|
+
rate_estimates = []
|
75
|
+
xml.elements.each('hash/products/product') do |prod|
|
76
|
+
if( prod.get_text('packaging') == 'postage_only' )
|
77
|
+
rate_estimates << RateEstimate.new(origin,
|
78
|
+
destination,
|
79
|
+
@@name,
|
80
|
+
prod.get_text('service-group-description').to_s,
|
81
|
+
:total_price => prod.get_text('cost').to_s.to_f,
|
82
|
+
:currency => 'NZD',
|
83
|
+
:service_code => prod.get_text('code').to_s,
|
84
|
+
:package => package)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
RateResponse.new(true, "Success", Hash.from_xml(response), :rates => rate_estimates, :xml => response)
|
89
|
+
else
|
90
|
+
error_message = response_message(xml)
|
91
|
+
RateResponse.new(false, error_message, Hash.from_xml(response), :rates => rate_estimates, :xml => response)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def combine_rate_responses(rate_responses, packages)
|
96
|
+
#if there are any failed responses, return on that response
|
97
|
+
rate_responses.each do |r|
|
98
|
+
return r if !r.success?
|
99
|
+
end
|
100
|
+
|
101
|
+
#group rate estimates by delivery type so that we can exclude any incomplete delviery types
|
102
|
+
rate_estimate_delivery_types = {}
|
103
|
+
rate_responses.each do |rr|
|
104
|
+
rr.rate_estimates.each do |re|
|
105
|
+
(rate_estimate_delivery_types[re.service_code] ||= []) << re
|
106
|
+
end
|
107
|
+
end
|
108
|
+
rate_estimate_delivery_types.delete_if{ |type, re| re.size != packages.size }
|
109
|
+
|
110
|
+
#combine cost estimates for remaining packages
|
111
|
+
combined_rate_estimates = []
|
112
|
+
rate_estimate_delivery_types.each do |type, re|
|
113
|
+
total_price = re.sum(&:total_price)
|
114
|
+
r = re.first
|
115
|
+
combined_rate_estimates << RateEstimate.new(r.origin, r.destination, r.carrier,
|
116
|
+
r.service_name,
|
117
|
+
:total_price => total_price,
|
118
|
+
:currency => r.currency,
|
119
|
+
:service_code => r.service_code,
|
120
|
+
:packages => packages)
|
121
|
+
end
|
122
|
+
RateResponse.new(true, "Success", {}, :rates => combined_rate_estimates)
|
123
|
+
end
|
124
|
+
|
125
|
+
def response_success?(xml)
|
126
|
+
xml.get_text('hash/status').to_s == 'success'
|
127
|
+
end
|
128
|
+
|
129
|
+
def response_message(xml)
|
130
|
+
if response_success?(xml)
|
131
|
+
'Success'
|
132
|
+
else
|
133
|
+
xml.get_text('hash/message').to_s
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'builder'
|
3
|
+
|
4
|
+
module ActiveMerchant
|
5
|
+
module Shipping
|
6
|
+
class Shipwire < Carrier
|
7
|
+
self.retry_safe = true
|
8
|
+
|
9
|
+
cattr_reader :name
|
10
|
+
@@name = "Shipwire"
|
11
|
+
|
12
|
+
URL = 'https://api.shipwire.com/exec/RateServices.php'
|
13
|
+
SCHEMA_URL = 'http://www.shipwire.com/exec/download/RateRequest.dtd'
|
14
|
+
WAREHOUSES = { 'CHI' => 'Chicago',
|
15
|
+
'LAX' => 'Los Angeles',
|
16
|
+
'REN' => 'Reno',
|
17
|
+
'VAN' => 'Vancouver',
|
18
|
+
'TOR' => 'Toronto',
|
19
|
+
'UK' => 'United Kingdom'
|
20
|
+
}
|
21
|
+
|
22
|
+
CARRIERS = [ "UPS", "USPS", "FedEx", "Royal Mail", "Parcelforce", "Pharos", "Eurotrux", "Canada Post", "DHL" ]
|
23
|
+
|
24
|
+
SUCCESS = "OK"
|
25
|
+
SUCCESS_MESSAGE = "Successfully received the shipping rates"
|
26
|
+
NO_RATES_MESSAGE = "No shipping rates could be found for the destination address"
|
27
|
+
REQUIRED_OPTIONS = [:login, :password].freeze
|
28
|
+
|
29
|
+
def find_rates(origin, destination, packages, options = {})
|
30
|
+
requires!(options, :items)
|
31
|
+
commit(origin, destination, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def valid_credentials?
|
35
|
+
location = self.class.default_location
|
36
|
+
find_rates(location, location, Package.new(100, [5,15,30]),
|
37
|
+
:items => [ { :sku => '', :quantity => 1 } ]
|
38
|
+
)
|
39
|
+
rescue ActiveMerchant::Shipping::ResponseError => e
|
40
|
+
e.message != "Could not verify e-mail/password combination"
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def requirements
|
45
|
+
REQUIRED_OPTIONS
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_request(destination, options)
|
49
|
+
xml = Builder::XmlMarkup.new
|
50
|
+
xml.instruct!
|
51
|
+
xml.declare! :DOCTYPE, :RateRequest, :SYSTEM, SCHEMA_URL
|
52
|
+
xml.tag! 'RateRequest' do
|
53
|
+
add_credentials(xml)
|
54
|
+
add_order(xml, destination, options)
|
55
|
+
end
|
56
|
+
xml.target!
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_credentials(xml)
|
60
|
+
xml.tag! 'EmailAddress', @options[:login]
|
61
|
+
xml.tag! 'Password', @options[:password]
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_order(xml, destination, options)
|
65
|
+
xml.tag! 'Order', :id => options[:order_id] do
|
66
|
+
xml.tag! 'Warehouse', options[:warehouse] || '00'
|
67
|
+
|
68
|
+
add_address(xml, destination)
|
69
|
+
Array(options[:items]).each_with_index do |line_item, index|
|
70
|
+
add_item(xml, line_item, index)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def add_address(xml, destination)
|
76
|
+
xml.tag! 'AddressInfo', :type => 'Ship' do
|
77
|
+
if destination.name.present?
|
78
|
+
xml.tag! 'Name' do
|
79
|
+
xml.tag! 'Full', destination.name
|
80
|
+
end
|
81
|
+
end
|
82
|
+
xml.tag! 'Address1', destination.address1
|
83
|
+
xml.tag! 'Address2', destination.address2 unless destination.address2.blank?
|
84
|
+
xml.tag! 'Address3', destination.address3 unless destination.address3.blank?
|
85
|
+
xml.tag! 'City', destination.city
|
86
|
+
xml.tag! 'State', destination.state unless destination.state.blank?
|
87
|
+
xml.tag! 'Country', destination.country_code
|
88
|
+
xml.tag! 'Zip', destination.zip unless destination.zip.blank?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Code is limited to 12 characters
|
93
|
+
def add_item(xml, item, index)
|
94
|
+
xml.tag! 'Item', :num => index do
|
95
|
+
xml.tag! 'Code', item[:sku]
|
96
|
+
xml.tag! 'Quantity', item[:quantity]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def commit(origin, destination, options)
|
101
|
+
request = build_request(destination, options)
|
102
|
+
save_request(request)
|
103
|
+
|
104
|
+
response = parse( ssl_post(URL, "RateRequestXML=#{CGI.escape(request)}") )
|
105
|
+
|
106
|
+
RateResponse.new(response["success"], response["message"], response,
|
107
|
+
:xml => response,
|
108
|
+
:rates => build_rate_estimates(response, origin, destination),
|
109
|
+
:request => last_request
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
def build_rate_estimates(response, origin, destination)
|
114
|
+
response["rates"].collect do |quote|
|
115
|
+
RateEstimate.new(origin, destination, carrier_for(quote["service"]), quote["service"],
|
116
|
+
:service_code => quote["method"],
|
117
|
+
:total_price => quote["cost"],
|
118
|
+
:currency => quote["currency"]
|
119
|
+
)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def carrier_for(service)
|
124
|
+
CARRIERS.dup.find{ |carrier| service.to_s =~ /^#{carrier}/i } || service.to_s.split(" ").first
|
125
|
+
end
|
126
|
+
|
127
|
+
def parse(xml)
|
128
|
+
response = {}
|
129
|
+
response["rates"] = []
|
130
|
+
|
131
|
+
document = REXML::Document.new(xml)
|
132
|
+
|
133
|
+
response["status"] = parse_child_text(document.root, "Status")
|
134
|
+
|
135
|
+
document.root.elements.each("Order/Quotes/Quote") do |e|
|
136
|
+
rate = {}
|
137
|
+
rate["method"] = e.attributes["method"]
|
138
|
+
rate["warehouse"] = parse_child_text(e, "Warehouse")
|
139
|
+
rate["service"] = parse_child_text(e, "Service")
|
140
|
+
rate["cost"] = parse_child_text(e, "Cost")
|
141
|
+
rate["currency"] = parse_child_attribute(e, "Cost", "currency")
|
142
|
+
response["rates"] << rate
|
143
|
+
end
|
144
|
+
|
145
|
+
if response["status"] == SUCCESS && response["rates"].any?
|
146
|
+
response["success"] = true
|
147
|
+
response["message"] = SUCCESS_MESSAGE
|
148
|
+
elsif response["status"] == SUCCESS && response["rates"].empty?
|
149
|
+
response["success"] = false
|
150
|
+
response["message"] = NO_RATES_MESSAGE
|
151
|
+
else
|
152
|
+
response["success"] = false
|
153
|
+
response["message"] = parse_child_text(document.root, "ErrorMessage")
|
154
|
+
end
|
155
|
+
|
156
|
+
response
|
157
|
+
end
|
158
|
+
|
159
|
+
def parse_child_text(parent, name)
|
160
|
+
if element = parent.elements[name]
|
161
|
+
element.text
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def parse_child_attribute(parent, name, attribute)
|
166
|
+
if element = parent.elements[name]
|
167
|
+
element.attributes[attribute]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,390 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module ActiveMerchant
|
4
|
+
module Shipping
|
5
|
+
class UPS < Carrier
|
6
|
+
self.retry_safe = true
|
7
|
+
|
8
|
+
cattr_accessor :default_options
|
9
|
+
cattr_reader :name
|
10
|
+
@@name = "UPS"
|
11
|
+
|
12
|
+
TEST_URL = 'https://wwwcie.ups.com'
|
13
|
+
LIVE_URL = 'https://www.ups.com'
|
14
|
+
|
15
|
+
RESOURCES = {
|
16
|
+
:rates => 'ups.app/xml/Rate',
|
17
|
+
:track => 'ups.app/xml/Track'
|
18
|
+
}
|
19
|
+
|
20
|
+
PICKUP_CODES = HashWithIndifferentAccess.new({
|
21
|
+
:daily_pickup => "01",
|
22
|
+
:customer_counter => "03",
|
23
|
+
:one_time_pickup => "06",
|
24
|
+
:on_call_air => "07",
|
25
|
+
:suggested_retail_rates => "11",
|
26
|
+
:letter_center => "19",
|
27
|
+
:air_service_center => "20"
|
28
|
+
})
|
29
|
+
|
30
|
+
CUSTOMER_CLASSIFICATIONS = HashWithIndifferentAccess.new({
|
31
|
+
:wholesale => "01",
|
32
|
+
:occasional => "03",
|
33
|
+
:retail => "04"
|
34
|
+
})
|
35
|
+
|
36
|
+
# these are the defaults described in the UPS API docs,
|
37
|
+
# but they don't seem to apply them under all circumstances,
|
38
|
+
# so we need to take matters into our own hands
|
39
|
+
DEFAULT_CUSTOMER_CLASSIFICATIONS = Hash.new do |hash,key|
|
40
|
+
hash[key] = case key.to_sym
|
41
|
+
when :daily_pickup then :wholesale
|
42
|
+
when :customer_counter then :retail
|
43
|
+
else
|
44
|
+
:occasional
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
DEFAULT_SERVICES = {
|
49
|
+
"01" => "UPS Next Day Air",
|
50
|
+
"02" => "UPS Second Day Air",
|
51
|
+
"03" => "UPS Ground",
|
52
|
+
"07" => "UPS Worldwide Express",
|
53
|
+
"08" => "UPS Worldwide Expedited",
|
54
|
+
"11" => "UPS Standard",
|
55
|
+
"12" => "UPS Three-Day Select",
|
56
|
+
"13" => "UPS Next Day Air Saver",
|
57
|
+
"14" => "UPS Next Day Air Early A.M.",
|
58
|
+
"54" => "UPS Worldwide Express Plus",
|
59
|
+
"59" => "UPS Second Day Air A.M.",
|
60
|
+
"65" => "UPS Saver",
|
61
|
+
"82" => "UPS Today Standard",
|
62
|
+
"83" => "UPS Today Dedicated Courier",
|
63
|
+
"84" => "UPS Today Intercity",
|
64
|
+
"85" => "UPS Today Express",
|
65
|
+
"86" => "UPS Today Express Saver"
|
66
|
+
}
|
67
|
+
|
68
|
+
CANADA_ORIGIN_SERVICES = {
|
69
|
+
"01" => "UPS Express",
|
70
|
+
"02" => "UPS Expedited",
|
71
|
+
"14" => "UPS Express Early A.M."
|
72
|
+
}
|
73
|
+
|
74
|
+
MEXICO_ORIGIN_SERVICES = {
|
75
|
+
"07" => "UPS Express",
|
76
|
+
"08" => "UPS Expedited",
|
77
|
+
"54" => "UPS Express Plus"
|
78
|
+
}
|
79
|
+
|
80
|
+
EU_ORIGIN_SERVICES = {
|
81
|
+
"07" => "UPS Express",
|
82
|
+
"08" => "UPS Expedited"
|
83
|
+
}
|
84
|
+
|
85
|
+
OTHER_NON_US_ORIGIN_SERVICES = {
|
86
|
+
"07" => "UPS Express"
|
87
|
+
}
|
88
|
+
|
89
|
+
# From http://en.wikipedia.org/w/index.php?title=European_Union&oldid=174718707 (Current as of November 30, 2007)
|
90
|
+
EU_COUNTRY_CODES = ["GB", "AT", "BE", "BG", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE"]
|
91
|
+
|
92
|
+
US_TERRITORIES_TREATED_AS_COUNTRIES = ["AS", "FM", "GU", "MH", "MP", "PW", "PR", "VI"]
|
93
|
+
|
94
|
+
def requirements
|
95
|
+
[:key, :login, :password]
|
96
|
+
end
|
97
|
+
|
98
|
+
def find_rates(origin, destination, packages, options={})
|
99
|
+
origin, destination = upsified_location(origin), upsified_location(destination)
|
100
|
+
options = @options.merge(options)
|
101
|
+
packages = Array(packages)
|
102
|
+
access_request = build_access_request
|
103
|
+
rate_request = build_rate_request(origin, destination, packages, options)
|
104
|
+
response = commit(:rates, save_request(access_request + rate_request), (options[:test] || false))
|
105
|
+
parse_rate_response(origin, destination, packages, response, options)
|
106
|
+
end
|
107
|
+
|
108
|
+
def find_tracking_info(tracking_number, options={})
|
109
|
+
options = @options.update(options)
|
110
|
+
access_request = build_access_request
|
111
|
+
tracking_request = build_tracking_request(tracking_number, options)
|
112
|
+
response = commit(:track, save_request(access_request + tracking_request), (options[:test] || false))
|
113
|
+
parse_tracking_response(response, options)
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
|
118
|
+
def upsified_location(location)
|
119
|
+
if location.country_code == 'US' && US_TERRITORIES_TREATED_AS_COUNTRIES.include?(location.state)
|
120
|
+
atts = {:country => location.state}
|
121
|
+
[:zip, :city, :address1, :address2, :address3, :phone, :fax, :address_type].each do |att|
|
122
|
+
atts[att] = location.send(att)
|
123
|
+
end
|
124
|
+
Location.new(atts)
|
125
|
+
else
|
126
|
+
location
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def build_access_request
|
131
|
+
xml_request = XmlNode.new('AccessRequest') do |access_request|
|
132
|
+
access_request << XmlNode.new('AccessLicenseNumber', @options[:key])
|
133
|
+
access_request << XmlNode.new('UserId', @options[:login])
|
134
|
+
access_request << XmlNode.new('Password', @options[:password])
|
135
|
+
end
|
136
|
+
xml_request.to_s
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_rate_request(origin, destination, packages, options={})
|
140
|
+
packages = Array(packages)
|
141
|
+
xml_request = XmlNode.new('RatingServiceSelectionRequest') do |root_node|
|
142
|
+
root_node << XmlNode.new('Request') do |request|
|
143
|
+
request << XmlNode.new('RequestAction', 'Rate')
|
144
|
+
request << XmlNode.new('RequestOption', 'Shop')
|
145
|
+
# not implemented: 'Rate' RequestOption to specify a single service query
|
146
|
+
# request << XmlNode.new('RequestOption', ((options[:service].nil? or options[:service] == :all) ? 'Shop' : 'Rate'))
|
147
|
+
end
|
148
|
+
|
149
|
+
pickup_type = options[:pickup_type] || :daily_pickup
|
150
|
+
|
151
|
+
root_node << XmlNode.new('PickupType') do |pickup_type_node|
|
152
|
+
pickup_type_node << XmlNode.new('Code', PICKUP_CODES[pickup_type])
|
153
|
+
# not implemented: PickupType/PickupDetails element
|
154
|
+
end
|
155
|
+
cc = options[:customer_classification] || DEFAULT_CUSTOMER_CLASSIFICATIONS[pickup_type]
|
156
|
+
root_node << XmlNode.new('CustomerClassification') do |cc_node|
|
157
|
+
cc_node << XmlNode.new('Code', CUSTOMER_CLASSIFICATIONS[cc])
|
158
|
+
end
|
159
|
+
|
160
|
+
root_node << XmlNode.new('Shipment') do |shipment|
|
161
|
+
# not implemented: Shipment/Description element
|
162
|
+
shipment << build_location_node('Shipper', (options[:shipper] || origin), options)
|
163
|
+
shipment << build_location_node('ShipTo', destination, options)
|
164
|
+
if options[:shipper] and options[:shipper] != origin
|
165
|
+
shipment << build_location_node('ShipFrom', origin, options)
|
166
|
+
end
|
167
|
+
|
168
|
+
# not implemented: * Shipment/ShipmentWeight element
|
169
|
+
# * Shipment/ReferenceNumber element
|
170
|
+
# * Shipment/Service element
|
171
|
+
# * Shipment/PickupDate element
|
172
|
+
# * Shipment/ScheduledDeliveryDate element
|
173
|
+
# * Shipment/ScheduledDeliveryTime element
|
174
|
+
# * Shipment/AlternateDeliveryTime element
|
175
|
+
# * Shipment/DocumentsOnly element
|
176
|
+
|
177
|
+
packages.each do |package|
|
178
|
+
imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))
|
179
|
+
|
180
|
+
shipment << XmlNode.new("Package") do |package_node|
|
181
|
+
|
182
|
+
# not implemented: * Shipment/Package/PackagingType element
|
183
|
+
# * Shipment/Package/Description element
|
184
|
+
|
185
|
+
package_node << XmlNode.new("PackagingType") do |packaging_type|
|
186
|
+
packaging_type << XmlNode.new("Code", '02')
|
187
|
+
end
|
188
|
+
|
189
|
+
package_node << XmlNode.new("Dimensions") do |dimensions|
|
190
|
+
dimensions << XmlNode.new("UnitOfMeasurement") do |units|
|
191
|
+
units << XmlNode.new("Code", imperial ? 'IN' : 'CM')
|
192
|
+
end
|
193
|
+
[:length,:width,:height].each do |axis|
|
194
|
+
value = ((imperial ? package.inches(axis) : package.cm(axis)).to_f*1000).round/1000.0 # 3 decimals
|
195
|
+
dimensions << XmlNode.new(axis.to_s.capitalize, [value,0.1].max)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
package_node << XmlNode.new("PackageWeight") do |package_weight|
|
200
|
+
package_weight << XmlNode.new("UnitOfMeasurement") do |units|
|
201
|
+
units << XmlNode.new("Code", imperial ? 'LBS' : 'KGS')
|
202
|
+
end
|
203
|
+
|
204
|
+
value = ((imperial ? package.lbs : package.kgs).to_f*1000).round/1000.0 # 3 decimals
|
205
|
+
package_weight << XmlNode.new("Weight", [value,0.1].max)
|
206
|
+
end
|
207
|
+
|
208
|
+
# not implemented: * Shipment/Package/LargePackageIndicator element
|
209
|
+
# * Shipment/Package/ReferenceNumber element
|
210
|
+
# * Shipment/Package/PackageServiceOptions element
|
211
|
+
# * Shipment/Package/AdditionalHandling element
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
|
216
|
+
# not implemented: * Shipment/ShipmentServiceOptions element
|
217
|
+
# * Shipment/RateInformation element
|
218
|
+
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
222
|
+
xml_request.to_s
|
223
|
+
end
|
224
|
+
|
225
|
+
def build_tracking_request(tracking_number, options={})
|
226
|
+
xml_request = XmlNode.new('TrackRequest') do |root_node|
|
227
|
+
root_node << XmlNode.new('Request') do |request|
|
228
|
+
request << XmlNode.new('RequestAction', 'Track')
|
229
|
+
request << XmlNode.new('RequestOption', '1')
|
230
|
+
end
|
231
|
+
root_node << XmlNode.new('TrackingNumber', tracking_number.to_s)
|
232
|
+
end
|
233
|
+
xml_request.to_s
|
234
|
+
end
|
235
|
+
|
236
|
+
def build_location_node(name,location,options={})
|
237
|
+
# not implemented: * Shipment/Shipper/Name element
|
238
|
+
# * Shipment/(ShipTo|ShipFrom)/CompanyName element
|
239
|
+
# * Shipment/(Shipper|ShipTo|ShipFrom)/AttentionName element
|
240
|
+
# * Shipment/(Shipper|ShipTo|ShipFrom)/TaxIdentificationNumber element
|
241
|
+
location_node = XmlNode.new(name) do |location_node|
|
242
|
+
location_node << XmlNode.new('PhoneNumber', location.phone.gsub(/[^\d]/,'')) unless location.phone.blank?
|
243
|
+
location_node << XmlNode.new('FaxNumber', location.fax.gsub(/[^\d]/,'')) unless location.fax.blank?
|
244
|
+
|
245
|
+
if name == 'Shipper' and (origin_account = @options[:origin_account] || options[:origin_account])
|
246
|
+
location_node << XmlNode.new('ShipperNumber', origin_account)
|
247
|
+
elsif name == 'ShipTo' and (destination_account = @options[:destination_account] || options[:destination_account])
|
248
|
+
location_node << XmlNode.new('ShipperAssignedIdentificationNumber', destination_account)
|
249
|
+
end
|
250
|
+
|
251
|
+
location_node << XmlNode.new('Address') do |address|
|
252
|
+
address << XmlNode.new("AddressLine1", location.address1) unless location.address1.blank?
|
253
|
+
address << XmlNode.new("AddressLine2", location.address2) unless location.address2.blank?
|
254
|
+
address << XmlNode.new("AddressLine3", location.address3) unless location.address3.blank?
|
255
|
+
address << XmlNode.new("City", location.city) unless location.city.blank?
|
256
|
+
address << XmlNode.new("StateProvinceCode", location.province) unless location.province.blank?
|
257
|
+
# StateProvinceCode required for negotiated rates but not otherwise, for some reason
|
258
|
+
address << XmlNode.new("PostalCode", location.postal_code) unless location.postal_code.blank?
|
259
|
+
address << XmlNode.new("CountryCode", location.country_code(:alpha2)) unless location.country_code(:alpha2).blank?
|
260
|
+
address << XmlNode.new("ResidentialAddressIndicator", true) unless location.commercial? # the default should be that UPS returns residential rates for destinations that it doesn't know about
|
261
|
+
# not implemented: Shipment/(Shipper|ShipTo|ShipFrom)/Address/ResidentialAddressIndicator element
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def parse_rate_response(origin, destination, packages, response, options={})
|
267
|
+
rates = []
|
268
|
+
|
269
|
+
xml = REXML::Document.new(response)
|
270
|
+
success = response_success?(xml)
|
271
|
+
message = response_message(xml)
|
272
|
+
|
273
|
+
if success
|
274
|
+
rate_estimates = []
|
275
|
+
|
276
|
+
xml.elements.each('/*/RatedShipment') do |rated_shipment|
|
277
|
+
service_code = rated_shipment.get_text('Service/Code').to_s
|
278
|
+
rate_estimates << RateEstimate.new(origin, destination, @@name,
|
279
|
+
service_name_for(origin, service_code),
|
280
|
+
:total_price => rated_shipment.get_text('TotalCharges/MonetaryValue').to_s.to_f,
|
281
|
+
:currency => rated_shipment.get_text('TotalCharges/CurrencyCode').to_s,
|
282
|
+
:service_code => service_code,
|
283
|
+
:packages => packages)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
RateResponse.new(success, message, Hash.from_xml(response).values.first, :rates => rate_estimates, :xml => response, :request => last_request)
|
287
|
+
end
|
288
|
+
|
289
|
+
def parse_tracking_response(response, options={})
|
290
|
+
xml = REXML::Document.new(response)
|
291
|
+
success = response_success?(xml)
|
292
|
+
message = response_message(xml)
|
293
|
+
|
294
|
+
if success
|
295
|
+
tracking_number, origin, destination = nil
|
296
|
+
shipment_events = []
|
297
|
+
|
298
|
+
first_shipment = xml.elements['/*/Shipment']
|
299
|
+
first_package = first_shipment.elements['Package']
|
300
|
+
tracking_number = first_shipment.get_text('ShipmentIdentificationNumber | Package/TrackingNumber').to_s
|
301
|
+
|
302
|
+
origin, destination = %w{Shipper ShipTo}.map do |location|
|
303
|
+
location_from_address_node(first_shipment.elements["#{location}/Address"])
|
304
|
+
end
|
305
|
+
|
306
|
+
activities = first_package.get_elements('Activity')
|
307
|
+
unless activities.empty?
|
308
|
+
shipment_events = activities.map do |activity|
|
309
|
+
description = activity.get_text('Status/StatusType/Description').to_s
|
310
|
+
zoneless_time = if (time = activity.get_text('Time')) &&
|
311
|
+
(date = activity.get_text('Date'))
|
312
|
+
time, date = time.to_s, date.to_s
|
313
|
+
hour, minute, second = time.scan(/\d{2}/)
|
314
|
+
year, month, day = date[0..3], date[4..5], date[6..7]
|
315
|
+
Time.utc(year, month, day, hour, minute, second)
|
316
|
+
end
|
317
|
+
location = location_from_address_node(activity.elements['ActivityLocation/Address'])
|
318
|
+
ShipmentEvent.new(description, zoneless_time, location)
|
319
|
+
end
|
320
|
+
|
321
|
+
shipment_events = shipment_events.sort_by(&:time)
|
322
|
+
|
323
|
+
if origin
|
324
|
+
first_event = shipment_events[0]
|
325
|
+
same_country = origin.country_code(:alpha2) == first_event.location.country_code(:alpha2)
|
326
|
+
same_or_blank_city = first_event.location.city.blank? or first_event.location.city == origin.city
|
327
|
+
origin_event = ShipmentEvent.new(first_event.name, first_event.time, origin)
|
328
|
+
if same_country and same_or_blank_city
|
329
|
+
shipment_events[0] = origin_event
|
330
|
+
else
|
331
|
+
shipment_events.unshift(origin_event)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
if shipment_events.last.name.downcase == 'delivered'
|
335
|
+
shipment_events[-1] = ShipmentEvent.new(shipment_events.last.name, shipment_events.last.time, destination)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|
340
|
+
TrackingResponse.new(success, message, Hash.from_xml(response).values.first,
|
341
|
+
:xml => response,
|
342
|
+
:request => last_request,
|
343
|
+
:shipment_events => shipment_events,
|
344
|
+
:origin => origin,
|
345
|
+
:destination => destination,
|
346
|
+
:tracking_number => tracking_number)
|
347
|
+
end
|
348
|
+
|
349
|
+
def location_from_address_node(address)
|
350
|
+
return nil unless address
|
351
|
+
Location.new(
|
352
|
+
:country => node_text_or_nil(address.elements['CountryCode']),
|
353
|
+
:postal_code => node_text_or_nil(address.elements['PostalCode']),
|
354
|
+
:province => node_text_or_nil(address.elements['StateProvinceCode']),
|
355
|
+
:city => node_text_or_nil(address.elements['City']),
|
356
|
+
:address1 => node_text_or_nil(address.elements['AddressLine1']),
|
357
|
+
:address2 => node_text_or_nil(address.elements['AddressLine2']),
|
358
|
+
:address3 => node_text_or_nil(address.elements['AddressLine3'])
|
359
|
+
)
|
360
|
+
end
|
361
|
+
|
362
|
+
def response_success?(xml)
|
363
|
+
xml.get_text('/*/Response/ResponseStatusCode').to_s == '1'
|
364
|
+
end
|
365
|
+
|
366
|
+
def response_message(xml)
|
367
|
+
xml.get_text('/*/Response/Error/ErrorDescription | /*/Response/ResponseStatusDescription').to_s
|
368
|
+
end
|
369
|
+
|
370
|
+
def commit(action, request, test = false)
|
371
|
+
ssl_post("#{test ? TEST_URL : LIVE_URL}/#{RESOURCES[action]}", request)
|
372
|
+
end
|
373
|
+
|
374
|
+
|
375
|
+
def service_name_for(origin, code)
|
376
|
+
origin = origin.country_code(:alpha2)
|
377
|
+
|
378
|
+
name = case origin
|
379
|
+
when "CA" then CANADA_ORIGIN_SERVICES[code]
|
380
|
+
when "MX" then MEXICO_ORIGIN_SERVICES[code]
|
381
|
+
when *EU_COUNTRY_CODES then EU_ORIGIN_SERVICES[code]
|
382
|
+
end
|
383
|
+
|
384
|
+
name ||= OTHER_NON_US_ORIGIN_SERVICES[code] unless name == 'US'
|
385
|
+
name ||= DEFAULT_SERVICES[code]
|
386
|
+
end
|
387
|
+
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|