active_shipping 0.11.2 → 0.12.0
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.
- checksums.yaml +7 -0
- data/lib/active_shipping/shipping/carriers.rb +3 -2
- data/lib/active_shipping/shipping/carriers/canada_post_pws.rb +9 -5
- data/lib/active_shipping/shipping/carriers/fedex.rb +2 -11
- data/lib/active_shipping/shipping/carriers/stamps.rb +876 -0
- data/lib/active_shipping/shipping/carriers/ups.rb +7 -1
- data/lib/active_shipping/shipping/carriers/usps.rb +1 -0
- data/lib/active_shipping/shipping/rate_estimate.rb +1 -1
- data/lib/active_shipping/version.rb +1 -1
- metadata +41 -67
- data/lib/active_shipping/shipping/carriers/ups.rb.orig +0 -456
- data/lib/active_shipping/shipping/carriers/usps.rb.orig +0 -616
@@ -337,11 +337,17 @@ module ActiveMerchant
|
|
337
337
|
tracking_number = first_shipment.get_text('ShipmentIdentificationNumber | Package/TrackingNumber').to_s
|
338
338
|
|
339
339
|
# Build status hash
|
340
|
-
|
340
|
+
status_nodes = first_package.elements.to_a('Activity/Status/StatusType')
|
341
|
+
|
342
|
+
# Prefer a delivery node
|
343
|
+
status_node = status_nodes.detect { |x| x.get_text('Code').to_s == 'D' }
|
344
|
+
status_node ||= status_nodes.first
|
345
|
+
|
341
346
|
status_code = status_node.get_text('Code').to_s
|
342
347
|
status_description = status_node.get_text('Description').to_s
|
343
348
|
status = TRACKING_STATUS_CODES[status_code]
|
344
349
|
|
350
|
+
|
345
351
|
if status_description =~ /out.*delivery/i
|
346
352
|
status = :out_for_delivery
|
347
353
|
end
|
@@ -241,6 +241,7 @@ module ActiveMerchant
|
|
241
241
|
|
242
242
|
# Once the address verification API is implemented, remove this and have valid_credentials? build the request using that instead.
|
243
243
|
def canned_address_verification_works?
|
244
|
+
return false unless @options[:login]
|
244
245
|
request = "%3CCarrierPickupAvailabilityRequest%20USERID=%22#{URI.encode(@options[:login])}%22%3E%20%0A%3CFirmName%3EABC%20Corp.%3C/FirmName%3E%20%0A%3CSuiteOrApt%3ESuite%20777%3C/SuiteOrApt%3E%20%0A%3CAddress2%3E1390%20Market%20Street%3C/Address2%3E%20%0A%3CUrbanization%3E%3C/Urbanization%3E%20%0A%3CCity%3EHouston%3C/City%3E%20%0A%3CState%3ETX%3C/State%3E%20%0A%3CZIP5%3E77058%3C/ZIP5%3E%20%0A%3CZIP4%3E1234%3C/ZIP4%3E%20%0A%3C/CarrierPickupAvailabilityRequest%3E%0A"
|
245
246
|
# expected_hash = {"CarrierPickupAvailabilityResponse"=>{"City"=>"HOUSTON", "Address2"=>"1390 Market Street", "FirmName"=>"ABC Corp.", "State"=>"TX", "Date"=>"3/1/2004", "DayOfWeek"=>"Monday", "Urbanization"=>nil, "ZIP4"=>"1234", "ZIP5"=>"77058", "CarrierRoute"=>"C", "SuiteOrApt"=>"Suite 777"}}
|
246
247
|
xml = REXML::Document.new(commit(:test, request, true))
|
@@ -26,7 +26,7 @@ module ActiveMerchant #:nodoc:
|
|
26
26
|
end
|
27
27
|
@total_price = Package.cents_from(options[:total_price])
|
28
28
|
@negotiated_rate = options[:negotiated_rate] ? Package.cents_from(options[:negotiated_rate]) : nil
|
29
|
-
@currency = options[:currency]
|
29
|
+
@currency = ActiveMerchant::CurrencyCode.standardize(options[:currency])
|
30
30
|
@delivery_range = options[:delivery_range] ? options[:delivery_range].map { |date| date_for(date) }.compact : []
|
31
31
|
@shipping_date = date_for(options[:shipping_date])
|
32
32
|
@delivery_date = @delivery_range.last
|
metadata
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_shipping
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.12.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- James MacAulay
|
@@ -12,166 +11,146 @@ authors:
|
|
12
11
|
autorequire:
|
13
12
|
bindir: bin
|
14
13
|
cert_chain: []
|
15
|
-
date: 2014-
|
14
|
+
date: 2014-05-12 00:00:00.000000000 Z
|
16
15
|
dependencies:
|
17
16
|
- !ruby/object:Gem::Dependency
|
18
17
|
name: activesupport
|
19
18
|
requirement: !ruby/object:Gem::Requirement
|
20
|
-
none: false
|
21
19
|
requirements:
|
22
|
-
- -
|
20
|
+
- - ">="
|
23
21
|
- !ruby/object:Gem::Version
|
24
22
|
version: 2.3.5
|
25
23
|
type: :runtime
|
26
24
|
prerelease: false
|
27
25
|
version_requirements: !ruby/object:Gem::Requirement
|
28
|
-
none: false
|
29
26
|
requirements:
|
30
|
-
- -
|
27
|
+
- - ">="
|
31
28
|
- !ruby/object:Gem::Version
|
32
29
|
version: 2.3.5
|
33
30
|
- !ruby/object:Gem::Dependency
|
34
31
|
name: i18n
|
35
32
|
requirement: !ruby/object:Gem::Requirement
|
36
|
-
none: false
|
37
33
|
requirements:
|
38
|
-
- -
|
34
|
+
- - ">="
|
39
35
|
- !ruby/object:Gem::Version
|
40
36
|
version: '0'
|
41
37
|
type: :runtime
|
42
38
|
prerelease: false
|
43
39
|
version_requirements: !ruby/object:Gem::Requirement
|
44
|
-
none: false
|
45
40
|
requirements:
|
46
|
-
- -
|
41
|
+
- - ">="
|
47
42
|
- !ruby/object:Gem::Version
|
48
43
|
version: '0'
|
49
44
|
- !ruby/object:Gem::Dependency
|
50
45
|
name: active_utils
|
51
46
|
requirement: !ruby/object:Gem::Requirement
|
52
|
-
none: false
|
53
47
|
requirements:
|
54
|
-
- -
|
48
|
+
- - "~>"
|
55
49
|
- !ruby/object:Gem::Version
|
56
|
-
version: 1.
|
50
|
+
version: 2.1.1
|
57
51
|
type: :runtime
|
58
52
|
prerelease: false
|
59
53
|
version_requirements: !ruby/object:Gem::Requirement
|
60
|
-
none: false
|
61
54
|
requirements:
|
62
|
-
- -
|
55
|
+
- - "~>"
|
63
56
|
- !ruby/object:Gem::Version
|
64
|
-
version: 1.
|
57
|
+
version: 2.1.1
|
65
58
|
- !ruby/object:Gem::Dependency
|
66
59
|
name: builder
|
67
60
|
requirement: !ruby/object:Gem::Requirement
|
68
|
-
none: false
|
69
61
|
requirements:
|
70
|
-
- -
|
62
|
+
- - ">="
|
71
63
|
- !ruby/object:Gem::Version
|
72
64
|
version: '0'
|
73
65
|
type: :runtime
|
74
66
|
prerelease: false
|
75
67
|
version_requirements: !ruby/object:Gem::Requirement
|
76
|
-
none: false
|
77
68
|
requirements:
|
78
|
-
- -
|
69
|
+
- - ">="
|
79
70
|
- !ruby/object:Gem::Version
|
80
71
|
version: '0'
|
81
72
|
- !ruby/object:Gem::Dependency
|
82
73
|
name: json
|
83
74
|
requirement: !ruby/object:Gem::Requirement
|
84
|
-
none: false
|
85
75
|
requirements:
|
86
|
-
- -
|
76
|
+
- - ">="
|
87
77
|
- !ruby/object:Gem::Version
|
88
78
|
version: 1.5.1
|
89
79
|
type: :runtime
|
90
80
|
prerelease: false
|
91
81
|
version_requirements: !ruby/object:Gem::Requirement
|
92
|
-
none: false
|
93
82
|
requirements:
|
94
|
-
- -
|
83
|
+
- - ">="
|
95
84
|
- !ruby/object:Gem::Version
|
96
85
|
version: 1.5.1
|
97
86
|
- !ruby/object:Gem::Dependency
|
98
87
|
name: minitest
|
99
88
|
requirement: !ruby/object:Gem::Requirement
|
100
|
-
none: false
|
101
89
|
requirements:
|
102
|
-
- - ~>
|
90
|
+
- - "~>"
|
103
91
|
- !ruby/object:Gem::Version
|
104
92
|
version: 4.7.5
|
105
93
|
type: :development
|
106
94
|
prerelease: false
|
107
95
|
version_requirements: !ruby/object:Gem::Requirement
|
108
|
-
none: false
|
109
96
|
requirements:
|
110
|
-
- - ~>
|
97
|
+
- - "~>"
|
111
98
|
- !ruby/object:Gem::Version
|
112
99
|
version: 4.7.5
|
113
100
|
- !ruby/object:Gem::Dependency
|
114
101
|
name: rake
|
115
102
|
requirement: !ruby/object:Gem::Requirement
|
116
|
-
none: false
|
117
103
|
requirements:
|
118
|
-
- -
|
104
|
+
- - ">="
|
119
105
|
- !ruby/object:Gem::Version
|
120
106
|
version: '0'
|
121
107
|
type: :development
|
122
108
|
prerelease: false
|
123
109
|
version_requirements: !ruby/object:Gem::Requirement
|
124
|
-
none: false
|
125
110
|
requirements:
|
126
|
-
- -
|
111
|
+
- - ">="
|
127
112
|
- !ruby/object:Gem::Version
|
128
113
|
version: '0'
|
129
114
|
- !ruby/object:Gem::Dependency
|
130
115
|
name: mocha
|
131
116
|
requirement: !ruby/object:Gem::Requirement
|
132
|
-
none: false
|
133
117
|
requirements:
|
134
|
-
- - ~>
|
118
|
+
- - "~>"
|
135
119
|
- !ruby/object:Gem::Version
|
136
120
|
version: 0.14.0
|
137
121
|
type: :development
|
138
122
|
prerelease: false
|
139
123
|
version_requirements: !ruby/object:Gem::Requirement
|
140
|
-
none: false
|
141
124
|
requirements:
|
142
|
-
- - ~>
|
125
|
+
- - "~>"
|
143
126
|
- !ruby/object:Gem::Version
|
144
127
|
version: 0.14.0
|
145
128
|
- !ruby/object:Gem::Dependency
|
146
129
|
name: timecop
|
147
130
|
requirement: !ruby/object:Gem::Requirement
|
148
|
-
none: false
|
149
131
|
requirements:
|
150
|
-
- -
|
132
|
+
- - ">="
|
151
133
|
- !ruby/object:Gem::Version
|
152
134
|
version: '0'
|
153
135
|
type: :development
|
154
136
|
prerelease: false
|
155
137
|
version_requirements: !ruby/object:Gem::Requirement
|
156
|
-
none: false
|
157
138
|
requirements:
|
158
|
-
- -
|
139
|
+
- - ">="
|
159
140
|
- !ruby/object:Gem::Version
|
160
141
|
version: '0'
|
161
142
|
- !ruby/object:Gem::Dependency
|
162
143
|
name: nokogiri
|
163
144
|
requirement: !ruby/object:Gem::Requirement
|
164
|
-
none: false
|
165
145
|
requirements:
|
166
|
-
- -
|
146
|
+
- - ">="
|
167
147
|
- !ruby/object:Gem::Version
|
168
148
|
version: '0'
|
169
149
|
type: :development
|
170
150
|
prerelease: false
|
171
151
|
version_requirements: !ruby/object:Gem::Requirement
|
172
|
-
none: false
|
173
152
|
requirements:
|
174
|
-
- -
|
153
|
+
- - ">="
|
175
154
|
- !ruby/object:Gem::Version
|
176
155
|
version: '0'
|
177
156
|
description: Get rates and tracking info from various shipping carriers.
|
@@ -181,8 +160,13 @@ executables: []
|
|
181
160
|
extensions: []
|
182
161
|
extra_rdoc_files: []
|
183
162
|
files:
|
163
|
+
- CHANGELOG
|
164
|
+
- MIT-LICENSE
|
165
|
+
- README.markdown
|
166
|
+
- lib/active_shipping.rb
|
184
167
|
- lib/active_shipping/shipping/base.rb
|
185
168
|
- lib/active_shipping/shipping/carrier.rb
|
169
|
+
- lib/active_shipping/shipping/carriers.rb
|
186
170
|
- lib/active_shipping/shipping/carriers/benchmark_carrier.rb
|
187
171
|
- lib/active_shipping/shipping/carriers/bogus_carrier.rb
|
188
172
|
- lib/active_shipping/shipping/carriers/canada_post.rb
|
@@ -191,11 +175,9 @@ files:
|
|
191
175
|
- lib/active_shipping/shipping/carriers/kunaki.rb
|
192
176
|
- lib/active_shipping/shipping/carriers/new_zealand_post.rb
|
193
177
|
- lib/active_shipping/shipping/carriers/shipwire.rb
|
178
|
+
- lib/active_shipping/shipping/carriers/stamps.rb
|
194
179
|
- lib/active_shipping/shipping/carriers/ups.rb
|
195
|
-
- lib/active_shipping/shipping/carriers/ups.rb.orig
|
196
180
|
- lib/active_shipping/shipping/carriers/usps.rb
|
197
|
-
- lib/active_shipping/shipping/carriers/usps.rb.orig
|
198
|
-
- lib/active_shipping/shipping/carriers.rb
|
199
181
|
- lib/active_shipping/shipping/errors.rb
|
200
182
|
- lib/active_shipping/shipping/location.rb
|
201
183
|
- lib/active_shipping/shipping/package.rb
|
@@ -207,55 +189,47 @@ files:
|
|
207
189
|
- lib/active_shipping/shipping/shipping_response.rb
|
208
190
|
- lib/active_shipping/shipping/tracking_response.rb
|
209
191
|
- lib/active_shipping/version.rb
|
210
|
-
- lib/active_shipping.rb
|
211
192
|
- lib/certs/eParcel.dtd
|
193
|
+
- lib/vendor/quantified/MIT-LICENSE
|
194
|
+
- lib/vendor/quantified/README.markdown
|
195
|
+
- lib/vendor/quantified/Rakefile
|
212
196
|
- lib/vendor/quantified/init.rb
|
197
|
+
- lib/vendor/quantified/lib/quantified.rb
|
213
198
|
- lib/vendor/quantified/lib/quantified/attribute.rb
|
214
199
|
- lib/vendor/quantified/lib/quantified/length.rb
|
215
200
|
- lib/vendor/quantified/lib/quantified/mass.rb
|
216
|
-
- lib/vendor/quantified/lib/quantified.rb
|
217
|
-
- lib/vendor/quantified/MIT-LICENSE
|
218
|
-
- lib/vendor/quantified/Rakefile
|
219
|
-
- lib/vendor/quantified/README.markdown
|
220
201
|
- lib/vendor/quantified/test/length_test.rb
|
221
202
|
- lib/vendor/quantified/test/mass_test.rb
|
222
203
|
- lib/vendor/quantified/test/test_helper.rb
|
223
204
|
- lib/vendor/test_helper.rb
|
205
|
+
- lib/vendor/xml_node/README
|
206
|
+
- lib/vendor/xml_node/Rakefile
|
224
207
|
- lib/vendor/xml_node/benchmark/bench_generation.rb
|
225
208
|
- lib/vendor/xml_node/init.rb
|
226
209
|
- lib/vendor/xml_node/lib/xml_node.rb
|
227
|
-
- lib/vendor/xml_node/Rakefile
|
228
|
-
- lib/vendor/xml_node/README
|
229
210
|
- lib/vendor/xml_node/test/test_generating.rb
|
230
211
|
- lib/vendor/xml_node/test/test_parsing.rb
|
231
|
-
- MIT-LICENSE
|
232
|
-
- README.markdown
|
233
|
-
- CHANGELOG
|
234
212
|
homepage: http://github.com/shopify/active_shipping
|
235
213
|
licenses: []
|
214
|
+
metadata: {}
|
236
215
|
post_install_message:
|
237
216
|
rdoc_options: []
|
238
217
|
require_paths:
|
239
218
|
- lib
|
240
219
|
required_ruby_version: !ruby/object:Gem::Requirement
|
241
|
-
none: false
|
242
220
|
requirements:
|
243
|
-
- -
|
221
|
+
- - ">="
|
244
222
|
- !ruby/object:Gem::Version
|
245
223
|
version: '0'
|
246
|
-
segments:
|
247
|
-
- 0
|
248
|
-
hash: 3769244612129544255
|
249
224
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
250
|
-
none: false
|
251
225
|
requirements:
|
252
|
-
- -
|
226
|
+
- - ">="
|
253
227
|
- !ruby/object:Gem::Version
|
254
228
|
version: 1.3.6
|
255
229
|
requirements: []
|
256
230
|
rubyforge_project: active_shipping
|
257
|
-
rubygems_version:
|
231
|
+
rubygems_version: 2.2.2
|
258
232
|
signing_key:
|
259
|
-
specification_version:
|
233
|
+
specification_version: 4
|
260
234
|
summary: Shipping API extension for Active Merchant
|
261
235
|
test_files: []
|
@@ -1,456 +0,0 @@
|
|
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://onlinetools.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
|
-
TRACKING_STATUS_CODES = HashWithIndifferentAccess.new({
|
90
|
-
'I' => :in_transit,
|
91
|
-
'D' => :delivered,
|
92
|
-
'X' => :exception,
|
93
|
-
'P' => :pickup,
|
94
|
-
'M' => :manifest_pickup
|
95
|
-
})
|
96
|
-
|
97
|
-
# From http://en.wikipedia.org/w/index.php?title=European_Union&oldid=174718707 (Current as of November 30, 2007)
|
98
|
-
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"]
|
99
|
-
|
100
|
-
US_TERRITORIES_TREATED_AS_COUNTRIES = ["AS", "FM", "GU", "MH", "MP", "PW", "PR", "VI"]
|
101
|
-
|
102
|
-
def requirements
|
103
|
-
[:key, :login, :password]
|
104
|
-
end
|
105
|
-
|
106
|
-
def find_rates(origin, destination, packages, options={})
|
107
|
-
origin, destination = upsified_location(origin), upsified_location(destination)
|
108
|
-
options = @options.merge(options)
|
109
|
-
packages = Array(packages)
|
110
|
-
access_request = build_access_request
|
111
|
-
rate_request = build_rate_request(origin, destination, packages, options)
|
112
|
-
response = commit(:rates, save_request(access_request + rate_request), (options[:test] || false))
|
113
|
-
parse_rate_response(origin, destination, packages, response, options)
|
114
|
-
end
|
115
|
-
|
116
|
-
def find_tracking_info(tracking_number, options={})
|
117
|
-
options = @options.update(options)
|
118
|
-
access_request = build_access_request
|
119
|
-
tracking_request = build_tracking_request(tracking_number, options)
|
120
|
-
response = commit(:track, save_request(access_request + tracking_request), (options[:test] || false))
|
121
|
-
parse_tracking_response(response, options)
|
122
|
-
end
|
123
|
-
|
124
|
-
protected
|
125
|
-
|
126
|
-
def upsified_location(location)
|
127
|
-
if location.country_code == 'US' && US_TERRITORIES_TREATED_AS_COUNTRIES.include?(location.state)
|
128
|
-
atts = {:country => location.state}
|
129
|
-
[:zip, :city, :address1, :address2, :address3, :phone, :fax, :address_type].each do |att|
|
130
|
-
atts[att] = location.send(att)
|
131
|
-
end
|
132
|
-
Location.new(atts)
|
133
|
-
else
|
134
|
-
location
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def build_access_request
|
139
|
-
xml_request = XmlNode.new('AccessRequest') do |access_request|
|
140
|
-
access_request << XmlNode.new('AccessLicenseNumber', @options[:key])
|
141
|
-
access_request << XmlNode.new('UserId', @options[:login])
|
142
|
-
access_request << XmlNode.new('Password', @options[:password])
|
143
|
-
end
|
144
|
-
xml_request.to_s
|
145
|
-
end
|
146
|
-
|
147
|
-
def build_rate_request(origin, destination, packages, options={})
|
148
|
-
packages = Array(packages)
|
149
|
-
xml_request = XmlNode.new('RatingServiceSelectionRequest') do |root_node|
|
150
|
-
root_node << XmlNode.new('Request') do |request|
|
151
|
-
request << XmlNode.new('RequestAction', 'Rate')
|
152
|
-
request << XmlNode.new('RequestOption', 'Shop')
|
153
|
-
# not implemented: 'Rate' RequestOption to specify a single service query
|
154
|
-
# request << XmlNode.new('RequestOption', ((options[:service].nil? or options[:service] == :all) ? 'Shop' : 'Rate'))
|
155
|
-
end
|
156
|
-
|
157
|
-
pickup_type = options[:pickup_type] || :daily_pickup
|
158
|
-
|
159
|
-
root_node << XmlNode.new('PickupType') do |pickup_type_node|
|
160
|
-
pickup_type_node << XmlNode.new('Code', PICKUP_CODES[pickup_type])
|
161
|
-
# not implemented: PickupType/PickupDetails element
|
162
|
-
end
|
163
|
-
cc = options[:customer_classification] || DEFAULT_CUSTOMER_CLASSIFICATIONS[pickup_type]
|
164
|
-
root_node << XmlNode.new('CustomerClassification') do |cc_node|
|
165
|
-
cc_node << XmlNode.new('Code', CUSTOMER_CLASSIFICATIONS[cc])
|
166
|
-
end
|
167
|
-
|
168
|
-
root_node << XmlNode.new('Shipment') do |shipment|
|
169
|
-
# not implemented: Shipment/Description element
|
170
|
-
shipment << build_location_node('Shipper', (options[:shipper] || origin), options)
|
171
|
-
shipment << build_location_node('ShipTo', destination, options)
|
172
|
-
if options[:shipper] and options[:shipper] != origin
|
173
|
-
shipment << build_location_node('ShipFrom', origin, options)
|
174
|
-
end
|
175
|
-
|
176
|
-
# not implemented: * Shipment/ShipmentWeight element
|
177
|
-
# * Shipment/ReferenceNumber element
|
178
|
-
# * Shipment/Service element
|
179
|
-
# * Shipment/PickupDate element
|
180
|
-
# * Shipment/ScheduledDeliveryDate element
|
181
|
-
# * Shipment/ScheduledDeliveryTime element
|
182
|
-
# * Shipment/AlternateDeliveryTime element
|
183
|
-
# * Shipment/DocumentsOnly element
|
184
|
-
|
185
|
-
packages.each do |package|
|
186
|
-
imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))
|
187
|
-
|
188
|
-
shipment << XmlNode.new("Package") do |package_node|
|
189
|
-
|
190
|
-
# not implemented: * Shipment/Package/PackagingType element
|
191
|
-
# * Shipment/Package/Description element
|
192
|
-
|
193
|
-
package_node << XmlNode.new("PackagingType") do |packaging_type|
|
194
|
-
packaging_type << XmlNode.new("Code", '02')
|
195
|
-
end
|
196
|
-
|
197
|
-
package_node << XmlNode.new("Dimensions") do |dimensions|
|
198
|
-
dimensions << XmlNode.new("UnitOfMeasurement") do |units|
|
199
|
-
units << XmlNode.new("Code", imperial ? 'IN' : 'CM')
|
200
|
-
end
|
201
|
-
[:length,:width,:height].each do |axis|
|
202
|
-
value = ((imperial ? package.inches(axis) : package.cm(axis)).to_f*1000).round/1000.0 # 3 decimals
|
203
|
-
dimensions << XmlNode.new(axis.to_s.capitalize, [value,0.1].max)
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
package_node << XmlNode.new("PackageWeight") do |package_weight|
|
208
|
-
package_weight << XmlNode.new("UnitOfMeasurement") do |units|
|
209
|
-
units << XmlNode.new("Code", imperial ? 'LBS' : 'KGS')
|
210
|
-
end
|
211
|
-
|
212
|
-
value = ((imperial ? package.lbs : package.kgs).to_f*1000).round/1000.0 # 3 decimals
|
213
|
-
package_weight << XmlNode.new("Weight", [value,0.1].max)
|
214
|
-
end
|
215
|
-
|
216
|
-
# not implemented: * Shipment/Package/LargePackageIndicator element
|
217
|
-
# * Shipment/Package/ReferenceNumber element
|
218
|
-
# * Shipment/Package/PackageServiceOptions element
|
219
|
-
# * Shipment/Package/AdditionalHandling element
|
220
|
-
end
|
221
|
-
|
222
|
-
end
|
223
|
-
|
224
|
-
# not implemented: * Shipment/ShipmentServiceOptions element
|
225
|
-
if options[:origin_account]
|
226
|
-
shipment << XmlNode.new("RateInformation") do |rate_info_node|
|
227
|
-
rate_info_node << XmlNode.new("NegotiatedRatesIndicator")
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
end
|
232
|
-
|
233
|
-
end
|
234
|
-
xml_request.to_s
|
235
|
-
end
|
236
|
-
|
237
|
-
def build_tracking_request(tracking_number, options={})
|
238
|
-
xml_request = XmlNode.new('TrackRequest') do |root_node|
|
239
|
-
root_node << XmlNode.new('Request') do |request|
|
240
|
-
request << XmlNode.new('RequestAction', 'Track')
|
241
|
-
request << XmlNode.new('RequestOption', '1')
|
242
|
-
end
|
243
|
-
root_node << XmlNode.new('TrackingNumber', tracking_number.to_s)
|
244
|
-
end
|
245
|
-
xml_request.to_s
|
246
|
-
end
|
247
|
-
|
248
|
-
def build_location_node(name,location,options={})
|
249
|
-
# not implemented: * Shipment/Shipper/Name element
|
250
|
-
# * Shipment/(ShipTo|ShipFrom)/CompanyName element
|
251
|
-
# * Shipment/(Shipper|ShipTo|ShipFrom)/AttentionName element
|
252
|
-
# * Shipment/(Shipper|ShipTo|ShipFrom)/TaxIdentificationNumber element
|
253
|
-
location_node = XmlNode.new(name) do |location_node|
|
254
|
-
location_node << XmlNode.new('PhoneNumber', location.phone.gsub(/[^\d]/,'')) unless location.phone.blank?
|
255
|
-
location_node << XmlNode.new('FaxNumber', location.fax.gsub(/[^\d]/,'')) unless location.fax.blank?
|
256
|
-
|
257
|
-
if name == 'Shipper' and (origin_account = @options[:origin_account] || options[:origin_account])
|
258
|
-
location_node << XmlNode.new('ShipperNumber', origin_account)
|
259
|
-
elsif name == 'ShipTo' and (destination_account = @options[:destination_account] || options[:destination_account])
|
260
|
-
location_node << XmlNode.new('ShipperAssignedIdentificationNumber', destination_account)
|
261
|
-
end
|
262
|
-
|
263
|
-
location_node << XmlNode.new('Address') do |address|
|
264
|
-
address << XmlNode.new("AddressLine1", location.address1) unless location.address1.blank?
|
265
|
-
address << XmlNode.new("AddressLine2", location.address2) unless location.address2.blank?
|
266
|
-
address << XmlNode.new("AddressLine3", location.address3) unless location.address3.blank?
|
267
|
-
address << XmlNode.new("City", location.city) unless location.city.blank?
|
268
|
-
address << XmlNode.new("StateProvinceCode", location.province) unless location.province.blank?
|
269
|
-
# StateProvinceCode required for negotiated rates but not otherwise, for some reason
|
270
|
-
address << XmlNode.new("PostalCode", location.postal_code) unless location.postal_code.blank?
|
271
|
-
address << XmlNode.new("CountryCode", location.country_code(:alpha2)) unless location.country_code(:alpha2).blank?
|
272
|
-
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
|
273
|
-
# not implemented: Shipment/(Shipper|ShipTo|ShipFrom)/Address/ResidentialAddressIndicator element
|
274
|
-
end
|
275
|
-
end
|
276
|
-
end
|
277
|
-
|
278
|
-
def parse_rate_response(origin, destination, packages, response, options={})
|
279
|
-
rates = []
|
280
|
-
File.open('/Users/timmy/code/active_shipping/foo.txt', 'w') {|f| f.write(response) }
|
281
|
-
xml = REXML::Document.new(response)
|
282
|
-
success = response_success?(xml)
|
283
|
-
message = response_message(xml)
|
284
|
-
|
285
|
-
if success
|
286
|
-
rate_estimates = []
|
287
|
-
|
288
|
-
xml.elements.each('/*/RatedShipment') do |rated_shipment|
|
289
|
-
service_code = rated_shipment.get_text('Service/Code').to_s
|
290
|
-
days_to_delivery = rated_shipment.get_text('GuaranteedDaysToDelivery').to_s.to_i
|
291
|
-
delivery_date = days_to_delivery >= 1 ? days_to_delivery.days.from_now.strftime("%Y-%m-%d") : nil
|
292
|
-
rate_estimates << RateEstimate.new(origin, destination, @@name,
|
293
|
-
service_name_for(origin, service_code),
|
294
|
-
:total_price => rated_shipment.get_text('TotalCharges/MonetaryValue').to_s.to_f,
|
295
|
-
:currency => rated_shipment.get_text('TotalCharges/CurrencyCode').to_s,
|
296
|
-
:service_code => service_code,
|
297
|
-
:packages => packages,
|
298
|
-
:delivery_range => [delivery_date],
|
299
|
-
:negotiated_rate => rated_shipment.get_text('NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue').to_s.to_f)
|
300
|
-
end
|
301
|
-
end
|
302
|
-
RateResponse.new(success, message, Hash.from_xml(response).values.first, :rates => rate_estimates, :xml => response, :request => last_request)
|
303
|
-
end
|
304
|
-
|
305
|
-
def parse_tracking_response(response, options={})
|
306
|
-
xml = REXML::Document.new(response)
|
307
|
-
success = response_success?(xml)
|
308
|
-
message = response_message(xml)
|
309
|
-
|
310
|
-
if success
|
311
|
-
tracking_number, origin, destination, status_code, status_description = nil
|
312
|
-
delivered, exception = false
|
313
|
-
exception_event = nil
|
314
|
-
shipment_events = []
|
315
|
-
status = {}
|
316
|
-
scheduled_delivery_date = nil
|
317
|
-
|
318
|
-
first_shipment = xml.elements['/*/Shipment']
|
319
|
-
first_package = first_shipment.elements['Package']
|
320
|
-
tracking_number = first_shipment.get_text('ShipmentIdentificationNumber | Package/TrackingNumber').to_s
|
321
|
-
|
322
|
-
# Build status hash
|
323
|
-
status_node = first_package.elements['Activity/Status/StatusType']
|
324
|
-
status_code = status_node.get_text('Code').to_s
|
325
|
-
status_description = status_node.get_text('Description').to_s
|
326
|
-
status = TRACKING_STATUS_CODES[status_code]
|
327
|
-
|
328
|
-
if status_description =~ /out.*delivery/i
|
329
|
-
status = :out_for_delivery
|
330
|
-
end
|
331
|
-
|
332
|
-
origin, destination = %w{Shipper ShipTo}.map do |location|
|
333
|
-
location_from_address_node(first_shipment.elements["#{location}/Address"])
|
334
|
-
end
|
335
|
-
|
336
|
-
# Get scheduled delivery date
|
337
|
-
unless status == :delivered
|
338
|
-
scheduled_delivery_date = parse_ups_datetime({
|
339
|
-
:date => first_shipment.get_text('ScheduledDeliveryDate'),
|
340
|
-
:time => nil
|
341
|
-
})
|
342
|
-
end
|
343
|
-
|
344
|
-
activities = first_package.get_elements('Activity')
|
345
|
-
unless activities.empty?
|
346
|
-
shipment_events = activities.map do |activity|
|
347
|
-
description = activity.get_text('Status/StatusType/Description').to_s
|
348
|
-
zoneless_time = if (time = activity.get_text('Time')) &&
|
349
|
-
(date = activity.get_text('Date'))
|
350
|
-
time, date = time.to_s, date.to_s
|
351
|
-
hour, minute, second = time.scan(/\d{2}/)
|
352
|
-
year, month, day = date[0..3], date[4..5], date[6..7]
|
353
|
-
Time.utc(year, month, day, hour, minute, second)
|
354
|
-
end
|
355
|
-
location = location_from_address_node(activity.elements['ActivityLocation/Address'])
|
356
|
-
ShipmentEvent.new(description, zoneless_time, location)
|
357
|
-
end
|
358
|
-
|
359
|
-
shipment_events = shipment_events.sort_by(&:time)
|
360
|
-
|
361
|
-
# UPS will sometimes archive a shipment, stripping all shipment activity except for the delivery
|
362
|
-
# event (see test/fixtures/xml/delivered_shipment_without_events_tracking_response.xml for an example).
|
363
|
-
# This adds an origin event to the shipment activity in such cases.
|
364
|
-
if origin && !(shipment_events.count == 1 && status == :delivered)
|
365
|
-
first_event = shipment_events[0]
|
366
|
-
same_country = origin.country_code(:alpha2) == first_event.location.country_code(:alpha2)
|
367
|
-
same_or_blank_city = first_event.location.city.blank? or first_event.location.city == origin.city
|
368
|
-
origin_event = ShipmentEvent.new(first_event.name, first_event.time, origin)
|
369
|
-
if same_country and same_or_blank_city
|
370
|
-
shipment_events[0] = origin_event
|
371
|
-
else
|
372
|
-
shipment_events.unshift(origin_event)
|
373
|
-
end
|
374
|
-
end
|
375
|
-
|
376
|
-
# Has the shipment been delivered?
|
377
|
-
if status == :delivered
|
378
|
-
if !destination
|
379
|
-
destination = shipment_events[-1].location
|
380
|
-
end
|
381
|
-
shipment_events[-1] = ShipmentEvent.new(shipment_events.last.name, shipment_events.last.time, destination)
|
382
|
-
end
|
383
|
-
end
|
384
|
-
|
385
|
-
end
|
386
|
-
TrackingResponse.new(success, message, Hash.from_xml(response).values.first,
|
387
|
-
:carrier => @@name,
|
388
|
-
:xml => response,
|
389
|
-
:request => last_request,
|
390
|
-
:status => status,
|
391
|
-
:status_code => status_code,
|
392
|
-
:status_description => status_description,
|
393
|
-
:scheduled_delivery_date => scheduled_delivery_date,
|
394
|
-
:shipment_events => shipment_events,
|
395
|
-
:delivered => delivered,
|
396
|
-
:exception => exception,
|
397
|
-
:exception_event => exception_event,
|
398
|
-
:origin => origin,
|
399
|
-
:destination => destination,
|
400
|
-
:tracking_number => tracking_number)
|
401
|
-
end
|
402
|
-
|
403
|
-
def location_from_address_node(address)
|
404
|
-
return nil unless address
|
405
|
-
Location.new(
|
406
|
-
:country => node_text_or_nil(address.elements['CountryCode']),
|
407
|
-
:postal_code => node_text_or_nil(address.elements['PostalCode']),
|
408
|
-
:province => node_text_or_nil(address.elements['StateProvinceCode']),
|
409
|
-
:city => node_text_or_nil(address.elements['City']),
|
410
|
-
:address1 => node_text_or_nil(address.elements['AddressLine1']),
|
411
|
-
:address2 => node_text_or_nil(address.elements['AddressLine2']),
|
412
|
-
:address3 => node_text_or_nil(address.elements['AddressLine3'])
|
413
|
-
)
|
414
|
-
end
|
415
|
-
|
416
|
-
def parse_ups_datetime(options = {})
|
417
|
-
time, date = options[:time].to_s, options[:date].to_s
|
418
|
-
if time.nil?
|
419
|
-
hour, minute, second = 0
|
420
|
-
else
|
421
|
-
hour, minute, second = time.scan(/\d{2}/)
|
422
|
-
end
|
423
|
-
year, month, day = date[0..3], date[4..5], date[6..7]
|
424
|
-
|
425
|
-
Time.utc(year, month, day, hour, minute, second)
|
426
|
-
end
|
427
|
-
|
428
|
-
def response_success?(xml)
|
429
|
-
xml.get_text('/*/Response/ResponseStatusCode').to_s == '1'
|
430
|
-
end
|
431
|
-
|
432
|
-
def response_message(xml)
|
433
|
-
xml.get_text('/*/Response/Error/ErrorDescription | /*/Response/ResponseStatusDescription').to_s
|
434
|
-
end
|
435
|
-
|
436
|
-
def commit(action, request, test = false)
|
437
|
-
ssl_post("#{test ? TEST_URL : LIVE_URL}/#{RESOURCES[action]}", request)
|
438
|
-
end
|
439
|
-
|
440
|
-
|
441
|
-
def service_name_for(origin, code)
|
442
|
-
origin = origin.country_code(:alpha2)
|
443
|
-
|
444
|
-
name = case origin
|
445
|
-
when "CA" then CANADA_ORIGIN_SERVICES[code]
|
446
|
-
when "MX" then MEXICO_ORIGIN_SERVICES[code]
|
447
|
-
when *EU_COUNTRY_CODES then EU_ORIGIN_SERVICES[code]
|
448
|
-
end
|
449
|
-
|
450
|
-
name ||= OTHER_NON_US_ORIGIN_SERVICES[code] unless name == 'US'
|
451
|
-
name ||= DEFAULT_SERVICES[code]
|
452
|
-
end
|
453
|
-
|
454
|
-
end
|
455
|
-
end
|
456
|
-
end
|