benhutton-active_shipping 0.9.13
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.
- 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
|