active_shipping 0.12.4 → 0.12.5
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 +4 -4
- data/lib/active_shipping.rb +2 -1
- data/lib/active_shipping/shipping/base.rb +2 -2
- data/lib/active_shipping/shipping/carrier.rb +16 -13
- data/lib/active_shipping/shipping/carriers/benchmark_carrier.rb +3 -4
- data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +1 -3
- data/lib/active_shipping/shipping/carriers/canada_post.rb +33 -44
- data/lib/active_shipping/shipping/carriers/canada_post_pws.rb +72 -81
- data/lib/active_shipping/shipping/carriers/fedex.rb +118 -109
- data/lib/active_shipping/shipping/carriers/kunaki.rb +33 -32
- data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +9 -16
- data/lib/active_shipping/shipping/carriers/shipwire.rb +36 -35
- data/lib/active_shipping/shipping/carriers/stamps.rb +39 -51
- data/lib/active_shipping/shipping/carriers/ups.rb +280 -116
- data/lib/active_shipping/shipping/carriers/ups.rb.orig +456 -0
- data/lib/active_shipping/shipping/carriers/usps.rb +145 -100
- data/lib/active_shipping/shipping/carriers/usps.rb.orig +616 -0
- data/lib/active_shipping/shipping/errors.rb +1 -1
- data/lib/active_shipping/shipping/label_response.rb +25 -0
- data/lib/active_shipping/shipping/location.rb +18 -16
- data/lib/active_shipping/shipping/package.rb +51 -54
- data/lib/active_shipping/shipping/rate_estimate.rb +10 -12
- data/lib/active_shipping/shipping/rate_response.rb +3 -7
- data/lib/active_shipping/shipping/response.rb +6 -9
- data/lib/active_shipping/shipping/shipment_event.rb +2 -4
- data/lib/active_shipping/shipping/shipment_packer.rb +32 -17
- data/lib/active_shipping/shipping/shipping_response.rb +2 -4
- data/lib/active_shipping/shipping/tracking_response.rb +3 -5
- data/lib/active_shipping/version.rb +1 -1
- data/lib/vendor/quantified/lib/quantified/attribute.rb +79 -80
- data/lib/vendor/quantified/lib/quantified/length.rb +5 -5
- data/lib/vendor/quantified/lib/quantified/mass.rb +4 -4
- data/lib/vendor/quantified/test/length_test.rb +19 -15
- data/lib/vendor/quantified/test/mass_test.rb +14 -14
- data/lib/vendor/quantified/test/test_helper.rb +1 -2
- data/lib/vendor/test_helper.rb +0 -1
- data/lib/vendor/xml_node/benchmark/bench_generation.rb +2 -4
- data/lib/vendor/xml_node/lib/xml_node.rb +54 -55
- data/lib/vendor/xml_node/test/test_generating.rb +23 -28
- data/lib/vendor/xml_node/test/test_parsing.rb +5 -8
- metadata +6 -25
- checksums.yaml.gz.sig +0 -1
- data.tar.gz.sig +0 -0
- metadata.gz.sig +0 -0
@@ -14,10 +14,12 @@ module ActiveMerchant
|
|
14
14
|
|
15
15
|
RESOURCES = {
|
16
16
|
:rates => 'ups.app/xml/Rate',
|
17
|
-
:track => 'ups.app/xml/Track'
|
17
|
+
:track => 'ups.app/xml/Track',
|
18
|
+
:ship_confirm => 'ups.app/xml/ShipConfirm',
|
19
|
+
:ship_accept => 'ups.app/xml/ShipAccept'
|
18
20
|
}
|
19
21
|
|
20
|
-
PICKUP_CODES = HashWithIndifferentAccess.new(
|
22
|
+
PICKUP_CODES = HashWithIndifferentAccess.new(
|
21
23
|
:daily_pickup => "01",
|
22
24
|
:customer_counter => "03",
|
23
25
|
:one_time_pickup => "06",
|
@@ -25,18 +27,18 @@ module ActiveMerchant
|
|
25
27
|
:suggested_retail_rates => "11",
|
26
28
|
:letter_center => "19",
|
27
29
|
:air_service_center => "20"
|
28
|
-
|
30
|
+
)
|
29
31
|
|
30
|
-
CUSTOMER_CLASSIFICATIONS = HashWithIndifferentAccess.new(
|
32
|
+
CUSTOMER_CLASSIFICATIONS = HashWithIndifferentAccess.new(
|
31
33
|
:wholesale => "01",
|
32
34
|
:occasional => "03",
|
33
35
|
:retail => "04"
|
34
|
-
|
36
|
+
)
|
35
37
|
|
36
38
|
# these are the defaults described in the UPS API docs,
|
37
39
|
# but they don't seem to apply them under all circumstances,
|
38
40
|
# so we need to take matters into our own hands
|
39
|
-
DEFAULT_CUSTOMER_CLASSIFICATIONS = Hash.new do |hash,key|
|
41
|
+
DEFAULT_CUSTOMER_CLASSIFICATIONS = Hash.new do |hash, key|
|
40
42
|
hash[key] = case key.to_sym
|
41
43
|
when :daily_pickup then :wholesale
|
42
44
|
when :customer_counter then :retail
|
@@ -86,24 +88,26 @@ module ActiveMerchant
|
|
86
88
|
"07" => "UPS Express"
|
87
89
|
}
|
88
90
|
|
89
|
-
TRACKING_STATUS_CODES = HashWithIndifferentAccess.new(
|
91
|
+
TRACKING_STATUS_CODES = HashWithIndifferentAccess.new(
|
90
92
|
'I' => :in_transit,
|
91
93
|
'D' => :delivered,
|
92
94
|
'X' => :exception,
|
93
95
|
'P' => :pickup,
|
94
96
|
'M' => :manifest_pickup
|
95
|
-
|
97
|
+
)
|
96
98
|
|
97
99
|
# From http://en.wikipedia.org/w/index.php?title=European_Union&oldid=174718707 (Current as of November 30, 2007)
|
98
|
-
EU_COUNTRY_CODES =
|
100
|
+
EU_COUNTRY_CODES = %w(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
101
|
|
100
|
-
US_TERRITORIES_TREATED_AS_COUNTRIES =
|
102
|
+
US_TERRITORIES_TREATED_AS_COUNTRIES = %w(AS FM GU MH MP PW PR VI)
|
103
|
+
|
104
|
+
IMPERIAL_COUNTRIES = %w(US LR MM)
|
101
105
|
|
102
106
|
def requirements
|
103
107
|
[:key, :login, :password]
|
104
108
|
end
|
105
109
|
|
106
|
-
def find_rates(origin, destination, packages, options={})
|
110
|
+
def find_rates(origin, destination, packages, options = {})
|
107
111
|
origin, destination = upsified_location(origin), upsified_location(destination)
|
108
112
|
options = @options.merge(options)
|
109
113
|
packages = Array(packages)
|
@@ -113,7 +117,7 @@ module ActiveMerchant
|
|
113
117
|
parse_rate_response(origin, destination, packages, response, options)
|
114
118
|
end
|
115
119
|
|
116
|
-
def find_tracking_info(tracking_number, options={})
|
120
|
+
def find_tracking_info(tracking_number, options = {})
|
117
121
|
options = @options.update(options)
|
118
122
|
access_request = build_access_request
|
119
123
|
tracking_request = build_tracking_request(tracking_number, options)
|
@@ -121,6 +125,46 @@ module ActiveMerchant
|
|
121
125
|
parse_tracking_response(response, options)
|
122
126
|
end
|
123
127
|
|
128
|
+
def create_shipment(origin, destination, packages, options = {})
|
129
|
+
options = @options.merge(options)
|
130
|
+
packages = Array(packages)
|
131
|
+
access_request = build_access_request
|
132
|
+
|
133
|
+
begin
|
134
|
+
|
135
|
+
# STEP 1: Confirm. Validation step, important for verifying price.
|
136
|
+
confirm_request = build_shipment_request(origin, destination, packages, options)
|
137
|
+
logger.debug(confirm_request) if logger
|
138
|
+
|
139
|
+
confirm_response = commit(:ship_confirm, save_request(access_request + confirm_request), (options[:test] || false))
|
140
|
+
logger.debug(confirm_response) if logger
|
141
|
+
|
142
|
+
# ... now, get the digest, it's needed to get the label. In theory,
|
143
|
+
# one could make decisions based on the price or some such to avoid
|
144
|
+
# surprises. This also has *no* error handling yet.
|
145
|
+
xml = parse_ship_confirm(confirm_response)
|
146
|
+
success = response_success?(xml)
|
147
|
+
message = response_message(xml)
|
148
|
+
digest = response_digest(xml)
|
149
|
+
raise message unless success
|
150
|
+
|
151
|
+
# STEP 2: Accept. Use shipment digest in first response to get the actual label.
|
152
|
+
accept_request = build_accept_request(digest, options)
|
153
|
+
logger.debug(accept_request) if logger
|
154
|
+
|
155
|
+
accept_response = commit(:ship_accept, save_request(access_request + accept_request), (options[:test] || false))
|
156
|
+
logger.debug(accept_response) if logger
|
157
|
+
|
158
|
+
# ...finally, build a map from the response that contains
|
159
|
+
# the label data and tracking information.
|
160
|
+
parse_ship_accept(accept_response)
|
161
|
+
|
162
|
+
rescue RuntimeError => e
|
163
|
+
raise "Could not obtain shipping label. #{e.message}."
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
124
168
|
protected
|
125
169
|
|
126
170
|
def upsified_location(location)
|
@@ -144,7 +188,7 @@ module ActiveMerchant
|
|
144
188
|
xml_request.to_s
|
145
189
|
end
|
146
190
|
|
147
|
-
def build_rate_request(origin, destination, packages, options={})
|
191
|
+
def build_rate_request(origin, destination, packages, options = {})
|
148
192
|
packages = Array(packages)
|
149
193
|
xml_request = XmlNode.new('RatingServiceSelectionRequest') do |root_node|
|
150
194
|
root_node << XmlNode.new('Request') do |request|
|
@@ -183,45 +227,8 @@ module ActiveMerchant
|
|
183
227
|
# * Shipment/DocumentsOnly element
|
184
228
|
|
185
229
|
packages.each do |package|
|
186
|
-
imperial
|
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
|
-
if package.value.present? && package.currency.present?
|
212
|
-
add_insured_node( package_node, currency:package.currency, value:(package.value.to_i/100) )
|
213
|
-
end
|
214
|
-
|
215
|
-
value = ((imperial ? package.lbs : package.kgs).to_f*1000).round/1000.0 # 3 decimals
|
216
|
-
package_weight << XmlNode.new("Weight", [value,0.1].max)
|
217
|
-
end
|
218
|
-
|
219
|
-
# not implemented: * Shipment/Package/LargePackageIndicator element
|
220
|
-
# * Shipment/Package/ReferenceNumber element
|
221
|
-
# * Shipment/Package/PackageServiceOptions element
|
222
|
-
# * Shipment/Package/AdditionalHandling element
|
223
|
-
end
|
224
|
-
|
230
|
+
options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
|
231
|
+
shipment << build_package_node(package, options)
|
225
232
|
end
|
226
233
|
|
227
234
|
# not implemented: * Shipment/ShipmentServiceOptions element
|
@@ -235,7 +242,110 @@ module ActiveMerchant
|
|
235
242
|
xml_request.to_s
|
236
243
|
end
|
237
244
|
|
238
|
-
|
245
|
+
# Build XML node to request a shipping label for the given packages.
|
246
|
+
#
|
247
|
+
# options:
|
248
|
+
# * origin_account: who will pay for the shipping label
|
249
|
+
# * customer_context: a "guid like substance" -- according to UPS
|
250
|
+
# * shipper: who is sending the package and where it should be returned
|
251
|
+
# if it is undeliverable.
|
252
|
+
# * ship_from: where the package is picked up.
|
253
|
+
# * service_code: default to '14'
|
254
|
+
# * service_descriptor: default to 'Next Day Air Early AM'
|
255
|
+
# * saturday_delivery: any truthy value causes this element to exist
|
256
|
+
# * optional_processing: 'validate' (blank) or 'nonvalidate' or blank
|
257
|
+
#
|
258
|
+
def build_shipment_request(origin, destination, packages, options = {})
|
259
|
+
# There are a lot of unimplemented elements, documenting all of them
|
260
|
+
# wouldprobably be unhelpful.
|
261
|
+
|
262
|
+
xml_request = XmlNode.new('ShipmentConfirmRequest') do |root_node|
|
263
|
+
root_node << XmlNode.new('Request') do |request|
|
264
|
+
# Required element and the text must be "ShipConfirm"
|
265
|
+
request << XmlNode.new('RequestAction', 'ShipConfirm')
|
266
|
+
# Required element cotnrols level of address validation.
|
267
|
+
request << XmlNode.new('RequestOption', options[:optional_processing] || 'validate')
|
268
|
+
# Optional element to identify transactions between client and server.
|
269
|
+
if options[:customer_context]
|
270
|
+
request << XmlNode.new('TransactionReference') do |refer|
|
271
|
+
refer << XmlNode.new('CustomerContext', options[:customer_context])
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
root_node << XmlNode.new('Shipment') do |shipment|
|
276
|
+
# Required element.
|
277
|
+
shipment << XmlNode.new('Service') do |service|
|
278
|
+
service << XmlNode.new('Code', options[:service_code] || '14')
|
279
|
+
service << XmlNode.new('Description', options[:service_description] || 'Next Day Air Early AM')
|
280
|
+
end
|
281
|
+
# Required element. The delivery destination.
|
282
|
+
shipment << build_location_node('ShipTo', destination, {})
|
283
|
+
# Required element. The company whose account is responsible for the label(s).
|
284
|
+
shipment << build_location_node('Shipper', options[:shipper] || origin, {})
|
285
|
+
# Required if pickup is different different from shipper's address.
|
286
|
+
if options[:ship_from]
|
287
|
+
shipment << build_location_node('ShipFrom', options[:ship_from], {})
|
288
|
+
end
|
289
|
+
# Optional.
|
290
|
+
if options[:saturday_delivery]
|
291
|
+
shipment << XmlNode.new('ShipmentServiceOptions') do |opts|
|
292
|
+
opts << XmlNode.new('SaturdayDelivery')
|
293
|
+
end
|
294
|
+
end
|
295
|
+
# Optional.
|
296
|
+
if options[:origin_account]
|
297
|
+
shipment << XmlNode.new('RateInformation') do |rate|
|
298
|
+
rate << XmlNode.new('NegotiatedRatesIndicator')
|
299
|
+
end
|
300
|
+
end
|
301
|
+
# Optional.
|
302
|
+
if options[:shipment] && options[:shipment][:reference_number]
|
303
|
+
shipment << XmlNode.new("ReferenceNumber") do |ref_node|
|
304
|
+
ref_node << XmlNode.new("Code", options[:shipment][:reference_number][:code] || "")
|
305
|
+
ref_node << XmlNode.new("Value", options[:shipment][:reference_number][:value])
|
306
|
+
end
|
307
|
+
end
|
308
|
+
# Conditionally required. Either this element or an ItemizedPaymentInformation
|
309
|
+
# is needed. However, only PaymentInformation is not implemented.
|
310
|
+
shipment << XmlNode.new('PaymentInformation') do |payment|
|
311
|
+
payment << XmlNode.new('Prepaid') do |prepay|
|
312
|
+
prepay << XmlNode.new('BillShipper') do |bill|
|
313
|
+
bill << XmlNode.new('AccountNumber', options[:origin_account])
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
# A request may specify multiple packages.
|
318
|
+
options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
|
319
|
+
packages.each do |package|
|
320
|
+
shipment << build_package_node(package, options)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
# I don't know all of the options that UPS supports for labels
|
324
|
+
# so I'm going with something very simple for now.
|
325
|
+
root_node << XmlNode.new('LabelSpecification') do |specification|
|
326
|
+
specification << XmlNode.new('LabelPrintMethod') do |print_method|
|
327
|
+
print_method << XmlNode.new('Code', 'GIF')
|
328
|
+
end
|
329
|
+
specification << XmlNode.new('HTTPUserAgent', 'Mozilla/4.5') # hmmm
|
330
|
+
specification << XmlNode.new('LabelImageFormat', 'GIF') do |image_format|
|
331
|
+
image_format << XmlNode.new('Code', 'GIF')
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
xml_request.to_s
|
336
|
+
end
|
337
|
+
|
338
|
+
def build_accept_request(digest, options = {})
|
339
|
+
xml_request = XmlNode.new('ShipmentAcceptRequest') do |root_node|
|
340
|
+
root_node << XmlNode.new('Request') do |request|
|
341
|
+
request << XmlNode.new('RequestAction', 'ShipAccept')
|
342
|
+
end
|
343
|
+
root_node << XmlNode.new('ShipmentDigest', digest)
|
344
|
+
end
|
345
|
+
xml_request.to_s
|
346
|
+
end
|
347
|
+
|
348
|
+
def build_tracking_request(tracking_number, options = {})
|
239
349
|
xml_request = XmlNode.new('TrackRequest') do |root_node|
|
240
350
|
root_node << XmlNode.new('Request') do |request|
|
241
351
|
request << XmlNode.new('RequestAction', 'Track')
|
@@ -246,28 +356,44 @@ module ActiveMerchant
|
|
246
356
|
xml_request.to_s
|
247
357
|
end
|
248
358
|
|
249
|
-
def build_location_node(name,location,options={})
|
359
|
+
def build_location_node(name, location, options = {})
|
250
360
|
# not implemented: * Shipment/Shipper/Name element
|
251
361
|
# * Shipment/(ShipTo|ShipFrom)/CompanyName element
|
252
362
|
# * Shipment/(Shipper|ShipTo|ShipFrom)/AttentionName element
|
253
363
|
# * Shipment/(Shipper|ShipTo|ShipFrom)/TaxIdentificationNumber element
|
254
|
-
|
255
|
-
|
256
|
-
|
364
|
+
XmlNode.new(name) do |location_node|
|
365
|
+
# You must specify the shipper name when creating labels.
|
366
|
+
if shipper_name = (options[:origin_name] || @options[:origin_name])
|
367
|
+
location_node << XmlNode.new('Name', shipper_name)
|
368
|
+
end
|
369
|
+
location_node << XmlNode.new('PhoneNumber', location.phone.gsub(/[^\d]/, '')) unless location.phone.blank?
|
370
|
+
location_node << XmlNode.new('FaxNumber', location.fax.gsub(/[^\d]/, '')) unless location.fax.blank?
|
257
371
|
|
258
|
-
if name == 'Shipper' and (origin_account =
|
372
|
+
if name == 'Shipper' and (origin_account = options[:origin_account] || @options[:origin_account])
|
259
373
|
location_node << XmlNode.new('ShipperNumber', origin_account)
|
260
|
-
elsif name == 'ShipTo' and (destination_account =
|
374
|
+
elsif name == 'ShipTo' and (destination_account = options[:destination_account] || @options[:destination_account])
|
261
375
|
location_node << XmlNode.new('ShipperAssignedIdentificationNumber', destination_account)
|
262
376
|
end
|
263
377
|
|
378
|
+
if name = location.company_name || location.name
|
379
|
+
location_node << XmlNode.new('CompanyName', name)
|
380
|
+
end
|
381
|
+
|
382
|
+
if phone = location.phone
|
383
|
+
location_node << XmlNode.new('PhoneNumber', phone)
|
384
|
+
end
|
385
|
+
|
386
|
+
if attn = location.name
|
387
|
+
location_node << XmlNode.new('AttentionName', attn)
|
388
|
+
end
|
389
|
+
|
264
390
|
location_node << XmlNode.new('Address') do |address|
|
265
391
|
address << XmlNode.new("AddressLine1", location.address1) unless location.address1.blank?
|
266
392
|
address << XmlNode.new("AddressLine2", location.address2) unless location.address2.blank?
|
267
393
|
address << XmlNode.new("AddressLine3", location.address3) unless location.address3.blank?
|
268
394
|
address << XmlNode.new("City", location.city) unless location.city.blank?
|
269
395
|
address << XmlNode.new("StateProvinceCode", location.province) unless location.province.blank?
|
270
|
-
|
396
|
+
# StateProvinceCode required for negotiated rates but not otherwise, for some reason
|
271
397
|
address << XmlNode.new("PostalCode", location.postal_code) unless location.postal_code.blank?
|
272
398
|
address << XmlNode.new("CountryCode", location.country_code(:alpha2)) unless location.country_code(:alpha2).blank?
|
273
399
|
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
|
@@ -276,24 +402,51 @@ module ActiveMerchant
|
|
276
402
|
end
|
277
403
|
end
|
278
404
|
|
279
|
-
def
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
405
|
+
def build_package_node(package, options = {})
|
406
|
+
XmlNode.new("Package") do |package_node|
|
407
|
+
|
408
|
+
# not implemented: * Shipment/Package/PackagingType element
|
409
|
+
# * Shipment/Package/Description element
|
410
|
+
|
411
|
+
package_node << XmlNode.new("PackagingType") do |packaging_type|
|
412
|
+
packaging_type << XmlNode.new("Code", '02')
|
286
413
|
end
|
287
|
-
|
288
|
-
|
289
|
-
|
414
|
+
|
415
|
+
package_node << XmlNode.new("Dimensions") do |dimensions|
|
416
|
+
dimensions << XmlNode.new("UnitOfMeasurement") do |units|
|
417
|
+
units << XmlNode.new("Code", options[:imperial] ? 'IN' : 'CM')
|
418
|
+
end
|
419
|
+
[:length, :width, :height].each do |axis|
|
420
|
+
value = ((options[:imperial] ? package.inches(axis) : package.cm(axis)).to_f * 1000).round / 1000.0 # 3 decimals
|
421
|
+
dimensions << XmlNode.new(axis.to_s.capitalize, [value, 0.1].max)
|
422
|
+
end
|
290
423
|
end
|
424
|
+
|
425
|
+
package_node << XmlNode.new("PackageWeight") do |package_weight|
|
426
|
+
package_weight << XmlNode.new("UnitOfMeasurement") do |units|
|
427
|
+
units << XmlNode.new("Code", options[:imperial] ? 'LBS' : 'KGS')
|
428
|
+
end
|
429
|
+
|
430
|
+
value = ((options[:imperial] ? package.lbs : package.kgs).to_f * 1000).round / 1000.0 # 3 decimals
|
431
|
+
package_weight << XmlNode.new("Weight", [value, 0.1].max)
|
432
|
+
end
|
433
|
+
|
434
|
+
if options[:package] && options[:package][:reference_number]
|
435
|
+
package_node << XmlNode.new("ReferenceNumber") do |ref_node|
|
436
|
+
ref_node << XmlNode.new("Code", options[:package][:reference_number][:code] || "")
|
437
|
+
ref_node << XmlNode.new("Value", options[:package][:reference_number][:value])
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
package_node
|
442
|
+
|
443
|
+
# not implemented: * Shipment/Package/LargePackageIndicator element
|
444
|
+
# * Shipment/Package/PackageServiceOptions element
|
445
|
+
# * Shipment/Package/AdditionalHandling element
|
291
446
|
end
|
292
447
|
end
|
293
448
|
|
294
|
-
def parse_rate_response(origin, destination, packages, response, options={})
|
295
|
-
rates = []
|
296
|
-
|
449
|
+
def parse_rate_response(origin, destination, packages, response, options = {})
|
297
450
|
xml = REXML::Document.new(response)
|
298
451
|
success = response_success?(xml)
|
299
452
|
message = response_message(xml)
|
@@ -306,31 +459,29 @@ module ActiveMerchant
|
|
306
459
|
days_to_delivery = rated_shipment.get_text('GuaranteedDaysToDelivery').to_s.to_i
|
307
460
|
days_to_delivery = nil if days_to_delivery == 0
|
308
461
|
rate_estimates << RateEstimate.new(origin, destination, @@name,
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
462
|
+
service_name_for(origin, service_code),
|
463
|
+
:total_price => rated_shipment.get_text('TotalCharges/MonetaryValue').to_s.to_f,
|
464
|
+
:insurance_price => rated_shipment.get_text('ServiceOptionsCharges/MonetaryValue').to_s.to_f,
|
465
|
+
:currency => rated_shipment.get_text('TotalCharges/CurrencyCode').to_s,
|
466
|
+
:service_code => service_code,
|
467
|
+
:packages => packages,
|
468
|
+
:delivery_range => [timestamp_from_business_day(days_to_delivery)],
|
469
|
+
:negotiated_rate => rated_shipment.get_text('NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue').to_s.to_f)
|
317
470
|
end
|
318
471
|
end
|
319
472
|
RateResponse.new(success, message, Hash.from_xml(response).values.first, :rates => rate_estimates, :xml => response, :request => last_request)
|
320
473
|
end
|
321
474
|
|
322
|
-
def parse_tracking_response(response, options={})
|
475
|
+
def parse_tracking_response(response, options = {})
|
323
476
|
xml = REXML::Document.new(response)
|
324
477
|
success = response_success?(xml)
|
325
478
|
message = response_message(xml)
|
326
479
|
|
327
480
|
if success
|
328
|
-
|
481
|
+
delivery_signature = nil
|
482
|
+
exception_event, scheduled_delivery_date, actual_delivery_date = nil
|
329
483
|
delivered, exception = false
|
330
|
-
exception_event = nil
|
331
484
|
shipment_events = []
|
332
|
-
status = {}
|
333
|
-
scheduled_delivery_date = nil
|
334
485
|
|
335
486
|
first_shipment = xml.elements['/*/Shipment']
|
336
487
|
first_package = first_shipment.elements['Package']
|
@@ -347,34 +498,27 @@ module ActiveMerchant
|
|
347
498
|
status_description = status_node.get_text('Description').to_s
|
348
499
|
status = TRACKING_STATUS_CODES[status_code]
|
349
500
|
|
350
|
-
|
351
501
|
if status_description =~ /out.*delivery/i
|
352
502
|
status = :out_for_delivery
|
353
503
|
end
|
354
504
|
|
355
|
-
origin, destination = %w
|
505
|
+
origin, destination = %w(Shipper ShipTo).map do |location|
|
356
506
|
location_from_address_node(first_shipment.elements["#{location}/Address"])
|
357
507
|
end
|
358
508
|
|
359
509
|
# Get scheduled delivery date
|
360
510
|
unless status == :delivered
|
361
|
-
scheduled_delivery_date = parse_ups_datetime(
|
511
|
+
scheduled_delivery_date = parse_ups_datetime(
|
362
512
|
:date => first_shipment.get_text('ScheduledDeliveryDate'),
|
363
513
|
:time => nil
|
364
|
-
|
514
|
+
)
|
365
515
|
end
|
366
516
|
|
367
517
|
activities = first_package.get_elements('Activity')
|
368
518
|
unless activities.empty?
|
369
519
|
shipment_events = activities.map do |activity|
|
370
520
|
description = activity.get_text('Status/StatusType/Description').to_s
|
371
|
-
zoneless_time =
|
372
|
-
(date = activity.get_text('Date'))
|
373
|
-
time, date = time.to_s, date.to_s
|
374
|
-
hour, minute, second = time.scan(/\d{2}/)
|
375
|
-
year, month, day = date[0..3], date[4..5], date[6..7]
|
376
|
-
Time.utc(year, month, day, hour, minute, second)
|
377
|
-
end
|
521
|
+
zoneless_time = parse_ups_datetime(:time => activity.get_text('Time'), :date => activity.get_text('Date'))
|
378
522
|
location = location_from_address_node(activity.elements['ActivityLocation/Address'])
|
379
523
|
ShipmentEvent.new(description, zoneless_time, location)
|
380
524
|
end
|
@@ -397,8 +541,12 @@ module ActiveMerchant
|
|
397
541
|
|
398
542
|
# Has the shipment been delivered?
|
399
543
|
if status == :delivered
|
400
|
-
|
401
|
-
|
544
|
+
delivered_activity = activities.first
|
545
|
+
delivery_signature = delivered_activity.get_text('ActivityLocation/SignedForByName').to_s
|
546
|
+
if delivered_activity.get_text('Status/StatusType/Code') == 'D'
|
547
|
+
actual_delivery_date = parse_ups_datetime(:date => delivered_activity.get_text('Date'), :time => delivered_activity.get_text('Time'))
|
548
|
+
end
|
549
|
+
unless destination
|
402
550
|
destination = shipment_events[-1].location
|
403
551
|
end
|
404
552
|
shipment_events[-1] = ShipmentEvent.new(shipment_events.last.name, shipment_events.last.time, destination)
|
@@ -407,21 +555,22 @@ module ActiveMerchant
|
|
407
555
|
|
408
556
|
end
|
409
557
|
TrackingResponse.new(success, message, Hash.from_xml(response).values.first,
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
558
|
+
:carrier => @@name,
|
559
|
+
:xml => response,
|
560
|
+
:request => last_request,
|
561
|
+
:status => status,
|
562
|
+
:status_code => status_code,
|
563
|
+
:status_description => status_description,
|
564
|
+
:delivery_signature => delivery_signature,
|
565
|
+
:scheduled_delivery_date => scheduled_delivery_date,
|
566
|
+
:actual_delivery_date => actual_delivery_date,
|
567
|
+
:shipment_events => shipment_events,
|
568
|
+
:delivered => delivered,
|
569
|
+
:exception => exception,
|
570
|
+
:exception_event => exception_event,
|
571
|
+
:origin => origin,
|
572
|
+
:destination => destination,
|
573
|
+
:tracking_number => tracking_number)
|
425
574
|
end
|
426
575
|
|
427
576
|
def location_from_address_node(address)
|
@@ -457,6 +606,22 @@ module ActiveMerchant
|
|
457
606
|
xml.get_text('/*/Response/Error/ErrorDescription | /*/Response/ResponseStatusDescription').to_s
|
458
607
|
end
|
459
608
|
|
609
|
+
def response_digest(xml)
|
610
|
+
xml.get_text('/*/ShipmentDigest').to_s
|
611
|
+
end
|
612
|
+
|
613
|
+
def parse_ship_confirm(response)
|
614
|
+
REXML::Document.new(response)
|
615
|
+
end
|
616
|
+
|
617
|
+
def parse_ship_accept(response)
|
618
|
+
xml = REXML::Document.new(response)
|
619
|
+
success = response_success?(xml)
|
620
|
+
message = response_message(xml)
|
621
|
+
|
622
|
+
LabelResponse.new(success, message, Hash.from_xml(response).values.first)
|
623
|
+
end
|
624
|
+
|
460
625
|
def commit(action, request, test = false)
|
461
626
|
ssl_post("#{test ? TEST_URL : LIVE_URL}/#{RESOURCES[action]}", request)
|
462
627
|
end
|
@@ -478,9 +643,8 @@ module ActiveMerchant
|
|
478
643
|
end
|
479
644
|
|
480
645
|
name ||= OTHER_NON_US_ORIGIN_SERVICES[code] unless name == 'US'
|
481
|
-
name
|
646
|
+
name || DEFAULT_SERVICES[code]
|
482
647
|
end
|
483
|
-
|
484
648
|
end
|
485
649
|
end
|
486
650
|
end
|