active_shipping 0.9.15 → 0.10.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.
@@ -337,18 +337,22 @@ module ActiveMerchant
337
337
  message = response_message(xml)
338
338
 
339
339
  if success
340
- tracking_number, origin, destination, status, status_code, status_description = nil
340
+ tracking_number, origin, destination, status, status_code, status_description, delivery_signature = nil
341
341
  shipment_events = []
342
342
 
343
343
  tracking_details = root_node.elements['TrackDetails']
344
344
  tracking_number = tracking_details.get_text('TrackingNumber').to_s
345
-
345
+
346
346
  status_code = tracking_details.get_text('StatusCode').to_s
347
347
  status_description = tracking_details.get_text('StatusDescription').to_s
348
348
  status = TRACKING_STATUS_CODES[status_code]
349
349
 
350
+ if status_code == 'DL' && tracking_details.get_text('SignatureProofOfDeliveryAvailable').to_s == 'true'
351
+ delivery_signature = tracking_details.get_text('DeliverySignatureName').to_s
352
+ end
353
+
350
354
  origin_node = tracking_details.elements['OriginLocationAddress']
351
-
355
+
352
356
  if origin_node
353
357
  origin = Location.new(
354
358
  :country => origin_node.get_text('CountryCode').to_s,
@@ -357,17 +361,7 @@ module ActiveMerchant
357
361
  )
358
362
  end
359
363
 
360
- destination_node = tracking_details.elements['DestinationAddress']
361
-
362
- if destination_node.nil?
363
- destination_node = tracking_details.elements['ActualDeliveryAddress']
364
- end
365
-
366
- destination = Location.new(
367
- :country => destination_node.get_text('CountryCode').to_s,
368
- :province => destination_node.get_text('StateOrProvinceCode').to_s,
369
- :city => destination_node.get_text('City').to_s
370
- )
364
+ destination = extract_destination(tracking_details)
371
365
 
372
366
  tracking_details.elements.each('Events') do |event|
373
367
  address = event.elements['Address']
@@ -380,11 +374,10 @@ module ActiveMerchant
380
374
 
381
375
  location = Location.new(:city => city, :state => state, :postal_code => zip_code, :country => country)
382
376
  description = event.get_text('EventDescription').to_s
383
-
384
- # for now, just assume UTC, even though it probably isn't
385
- time = Time.parse("#{event.get_text('Timestamp').to_s}")
386
- zoneless_time = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec)
387
-
377
+
378
+ time = Time.parse("#{event.get_text('Timestamp').to_s}")
379
+ zoneless_time = time.utc
380
+
388
381
  shipment_events << ShipmentEvent.new(description, zoneless_time, location)
389
382
  end
390
383
  shipment_events = shipment_events.sort_by(&:time)
@@ -398,6 +391,7 @@ module ActiveMerchant
398
391
  :status => status,
399
392
  :status_code => status_code,
400
393
  :status_description => status_description,
394
+ :delivery_signature => delivery_signature,
401
395
  :shipment_events => shipment_events,
402
396
  :origin => origin,
403
397
  :destination => destination,
@@ -448,6 +442,26 @@ module ActiveMerchant
448
442
  end
449
443
  results
450
444
  end
445
+
446
+ def extract_destination(document)
447
+ node = document.elements['DestinationAddress'] || document.elements['ActualDeliveryAddress']
448
+
449
+ args = if node
450
+ {
451
+ :country => node.get_text('CountryCode').to_s,
452
+ :province => node.get_text('StateOrProvinceCode').to_s,
453
+ :city => node.get_text('City').to_s
454
+ }
455
+ else
456
+ {
457
+ :country => ActiveMerchant::Country.new(:alpha2 => 'ZZ', :name => 'Unknown or Invalid Territory', :alpha3 => 'ZZZ', :numeric => '999'),
458
+ :province => 'unknown',
459
+ :city => 'unknown'
460
+ }
461
+ end
462
+
463
+ Location.new(args)
464
+ end
451
465
  end
452
466
  end
453
467
  end
@@ -4,22 +4,22 @@ module ActiveMerchant
4
4
  module Shipping
5
5
  class UPS < Carrier
6
6
  self.retry_safe = true
7
-
7
+
8
8
  cattr_accessor :default_options
9
9
  cattr_reader :name
10
10
  @@name = "UPS"
11
-
11
+
12
12
  TEST_URL = 'https://wwwcie.ups.com'
13
13
  LIVE_URL = 'https://onlinetools.ups.com'
14
-
14
+
15
15
  RESOURCES = {
16
16
  :rates => 'ups.app/xml/Rate',
17
17
  :track => 'ups.app/xml/Track'
18
18
  }
19
-
19
+
20
20
  PICKUP_CODES = HashWithIndifferentAccess.new({
21
21
  :daily_pickup => "01",
22
- :customer_counter => "03",
22
+ :customer_counter => "03",
23
23
  :one_time_pickup => "06",
24
24
  :on_call_air => "07",
25
25
  :suggested_retail_rates => "11",
@@ -29,7 +29,7 @@ module ActiveMerchant
29
29
 
30
30
  CUSTOMER_CLASSIFICATIONS = HashWithIndifferentAccess.new({
31
31
  :wholesale => "01",
32
- :occasional => "03",
32
+ :occasional => "03",
33
33
  :retail => "04"
34
34
  })
35
35
 
@@ -44,7 +44,7 @@ module ActiveMerchant
44
44
  :occasional
45
45
  end
46
46
  end
47
-
47
+
48
48
  DEFAULT_SERVICES = {
49
49
  "01" => "UPS Next Day Air",
50
50
  "02" => "UPS Second Day Air",
@@ -64,24 +64,24 @@ module ActiveMerchant
64
64
  "85" => "UPS Today Express",
65
65
  "86" => "UPS Today Express Saver"
66
66
  }
67
-
67
+
68
68
  CANADA_ORIGIN_SERVICES = {
69
69
  "01" => "UPS Express",
70
70
  "02" => "UPS Expedited",
71
71
  "14" => "UPS Express Early A.M."
72
72
  }
73
-
73
+
74
74
  MEXICO_ORIGIN_SERVICES = {
75
75
  "07" => "UPS Express",
76
76
  "08" => "UPS Expedited",
77
77
  "54" => "UPS Express Plus"
78
78
  }
79
-
79
+
80
80
  EU_ORIGIN_SERVICES = {
81
81
  "07" => "UPS Express",
82
82
  "08" => "UPS Expedited"
83
83
  }
84
-
84
+
85
85
  OTHER_NON_US_ORIGIN_SERVICES = {
86
86
  "07" => "UPS Express"
87
87
  }
@@ -96,13 +96,13 @@ module ActiveMerchant
96
96
 
97
97
  # From http://en.wikipedia.org/w/index.php?title=European_Union&oldid=174718707 (Current as of November 30, 2007)
98
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
-
99
+
100
100
  US_TERRITORIES_TREATED_AS_COUNTRIES = ["AS", "FM", "GU", "MH", "MP", "PW", "PR", "VI"]
101
-
101
+
102
102
  def requirements
103
103
  [:key, :login, :password]
104
104
  end
105
-
105
+
106
106
  def find_rates(origin, destination, packages, options={})
107
107
  origin, destination = upsified_location(origin), upsified_location(destination)
108
108
  options = @options.merge(options)
@@ -112,7 +112,7 @@ module ActiveMerchant
112
112
  response = commit(:rates, save_request(access_request + rate_request), (options[:test] || false))
113
113
  parse_rate_response(origin, destination, packages, response, options)
114
114
  end
115
-
115
+
116
116
  def find_tracking_info(tracking_number, options={})
117
117
  options = @options.update(options)
118
118
  access_request = build_access_request
@@ -120,9 +120,9 @@ module ActiveMerchant
120
120
  response = commit(:track, save_request(access_request + tracking_request), (options[:test] || false))
121
121
  parse_tracking_response(response, options)
122
122
  end
123
-
123
+
124
124
  protected
125
-
125
+
126
126
  def upsified_location(location)
127
127
  if location.country_code == 'US' && US_TERRITORIES_TREATED_AS_COUNTRIES.include?(location.state)
128
128
  atts = {:country => location.state}
@@ -134,7 +134,7 @@ module ActiveMerchant
134
134
  location
135
135
  end
136
136
  end
137
-
137
+
138
138
  def build_access_request
139
139
  xml_request = XmlNode.new('AccessRequest') do |access_request|
140
140
  access_request << XmlNode.new('AccessLicenseNumber', @options[:key])
@@ -143,7 +143,7 @@ module ActiveMerchant
143
143
  end
144
144
  xml_request.to_s
145
145
  end
146
-
146
+
147
147
  def build_rate_request(origin, destination, packages, options={})
148
148
  packages = Array(packages)
149
149
  xml_request = XmlNode.new('RatingServiceSelectionRequest') do |root_node|
@@ -153,9 +153,9 @@ module ActiveMerchant
153
153
  # not implemented: 'Rate' RequestOption to specify a single service query
154
154
  # request << XmlNode.new('RequestOption', ((options[:service].nil? or options[:service] == :all) ? 'Shop' : 'Rate'))
155
155
  end
156
-
156
+
157
157
  pickup_type = options[:pickup_type] || :daily_pickup
158
-
158
+
159
159
  root_node << XmlNode.new('PickupType') do |pickup_type_node|
160
160
  pickup_type_node << XmlNode.new('Code', PICKUP_CODES[pickup_type])
161
161
  # not implemented: PickupType/PickupDetails element
@@ -164,7 +164,7 @@ module ActiveMerchant
164
164
  root_node << XmlNode.new('CustomerClassification') do |cc_node|
165
165
  cc_node << XmlNode.new('Code', CUSTOMER_CLASSIFICATIONS[cc])
166
166
  end
167
-
167
+
168
168
  root_node << XmlNode.new('Shipment') do |shipment|
169
169
  # not implemented: Shipment/Description element
170
170
  shipment << build_location_node('Shipper', (options[:shipper] || origin), options)
@@ -172,28 +172,28 @@ module ActiveMerchant
172
172
  if options[:shipper] and options[:shipper] != origin
173
173
  shipment << build_location_node('ShipFrom', origin, options)
174
174
  end
175
-
175
+
176
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
-
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
185
  packages.each do |package|
186
186
  imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))
187
-
187
+
188
188
  shipment << XmlNode.new("Package") do |package_node|
189
-
189
+
190
190
  # not implemented: * Shipment/Package/PackagingType element
191
191
  # * Shipment/Package/Description element
192
-
192
+
193
193
  package_node << XmlNode.new("PackagingType") do |packaging_type|
194
194
  packaging_type << XmlNode.new("Code", '02')
195
195
  end
196
-
196
+
197
197
  package_node << XmlNode.new("Dimensions") do |dimensions|
198
198
  dimensions << XmlNode.new("UnitOfMeasurement") do |units|
199
199
  units << XmlNode.new("Code", imperial ? 'IN' : 'CM')
@@ -203,33 +203,35 @@ module ActiveMerchant
203
203
  dimensions << XmlNode.new(axis.to_s.capitalize, [value,0.1].max)
204
204
  end
205
205
  end
206
-
206
+
207
207
  package_node << XmlNode.new("PackageWeight") do |package_weight|
208
208
  package_weight << XmlNode.new("UnitOfMeasurement") do |units|
209
209
  units << XmlNode.new("Code", imperial ? 'LBS' : 'KGS')
210
210
  end
211
-
211
+
212
212
  value = ((imperial ? package.lbs : package.kgs).to_f*1000).round/1000.0 # 3 decimals
213
213
  package_weight << XmlNode.new("Weight", [value,0.1].max)
214
214
  end
215
-
215
+
216
216
  # not implemented: * Shipment/Package/LargePackageIndicator element
217
217
  # * Shipment/Package/ReferenceNumber element
218
218
  # * Shipment/Package/PackageServiceOptions element
219
- # * Shipment/Package/AdditionalHandling element
219
+ # * Shipment/Package/AdditionalHandling element
220
220
  end
221
-
221
+
222
222
  end
223
-
223
+
224
224
  # not implemented: * Shipment/ShipmentServiceOptions element
225
- # * Shipment/RateInformation element
226
-
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
227
230
  end
228
-
229
231
  end
230
232
  xml_request.to_s
231
233
  end
232
-
234
+
233
235
  def build_tracking_request(tracking_number, options={})
234
236
  xml_request = XmlNode.new('TrackRequest') do |root_node|
235
237
  root_node << XmlNode.new('Request') do |request|
@@ -240,7 +242,7 @@ module ActiveMerchant
240
242
  end
241
243
  xml_request.to_s
242
244
  end
243
-
245
+
244
246
  def build_location_node(name,location,options={})
245
247
  # not implemented: * Shipment/Shipper/Name element
246
248
  # * Shipment/(ShipTo|ShipFrom)/CompanyName element
@@ -249,13 +251,13 @@ module ActiveMerchant
249
251
  location_node = XmlNode.new(name) do |location_node|
250
252
  location_node << XmlNode.new('PhoneNumber', location.phone.gsub(/[^\d]/,'')) unless location.phone.blank?
251
253
  location_node << XmlNode.new('FaxNumber', location.fax.gsub(/[^\d]/,'')) unless location.fax.blank?
252
-
254
+
253
255
  if name == 'Shipper' and (origin_account = @options[:origin_account] || options[:origin_account])
254
256
  location_node << XmlNode.new('ShipperNumber', origin_account)
255
257
  elsif name == 'ShipTo' and (destination_account = @options[:destination_account] || options[:destination_account])
256
258
  location_node << XmlNode.new('ShipperAssignedIdentificationNumber', destination_account)
257
259
  end
258
-
260
+
259
261
  location_node << XmlNode.new('Address') do |address|
260
262
  address << XmlNode.new("AddressLine1", location.address1) unless location.address1.blank?
261
263
  address << XmlNode.new("AddressLine2", location.address2) unless location.address2.blank?
@@ -270,17 +272,17 @@ module ActiveMerchant
270
272
  end
271
273
  end
272
274
  end
273
-
275
+
274
276
  def parse_rate_response(origin, destination, packages, response, options={})
275
277
  rates = []
276
-
278
+
277
279
  xml = REXML::Document.new(response)
278
280
  success = response_success?(xml)
279
281
  message = response_message(xml)
280
-
282
+
281
283
  if success
282
284
  rate_estimates = []
283
-
285
+
284
286
  xml.elements.each('/*/RatedShipment') do |rated_shipment|
285
287
  service_code = rated_shipment.get_text('Service/Code').to_s
286
288
  days_to_delivery = rated_shipment.get_text('GuaranteedDaysToDelivery').to_s.to_i
@@ -292,17 +294,18 @@ module ActiveMerchant
292
294
  :currency => rated_shipment.get_text('TotalCharges/CurrencyCode').to_s,
293
295
  :service_code => service_code,
294
296
  :packages => packages,
295
- :delivery_range => [timestamp_from_business_day(days_to_delivery)])
297
+ :delivery_range => [timestamp_from_business_day(days_to_delivery)],
298
+ :negotiated_rate => rated_shipment.get_text('NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue').to_s.to_f)
296
299
  end
297
300
  end
298
301
  RateResponse.new(success, message, Hash.from_xml(response).values.first, :rates => rate_estimates, :xml => response, :request => last_request)
299
302
  end
300
-
303
+
301
304
  def parse_tracking_response(response, options={})
302
305
  xml = REXML::Document.new(response)
303
306
  success = response_success?(xml)
304
307
  message = response_message(xml)
305
-
308
+
306
309
  if success
307
310
  tracking_number, origin, destination, status_code, status_description = nil
308
311
  delivered, exception = false
@@ -314,7 +317,7 @@ module ActiveMerchant
314
317
  first_shipment = xml.elements['/*/Shipment']
315
318
  first_package = first_shipment.elements['Package']
316
319
  tracking_number = first_shipment.get_text('ShipmentIdentificationNumber | Package/TrackingNumber').to_s
317
-
320
+
318
321
  # Build status hash
319
322
  status_node = first_package.elements['Activity/Status/StatusType']
320
323
  status_code = status_node.get_text('Code').to_s
@@ -351,10 +354,10 @@ module ActiveMerchant
351
354
  location = location_from_address_node(activity.elements['ActivityLocation/Address'])
352
355
  ShipmentEvent.new(description, zoneless_time, location)
353
356
  end
354
-
357
+
355
358
  shipment_events = shipment_events.sort_by(&:time)
356
-
357
- # UPS will sometimes archive a shipment, stripping all shipment activity except for the delivery
359
+
360
+ # UPS will sometimes archive a shipment, stripping all shipment activity except for the delivery
358
361
  # event (see test/fixtures/xml/delivered_shipment_without_events_tracking_response.xml for an example).
359
362
  # This adds an origin event to the shipment activity in such cases.
360
363
  if origin && !(shipment_events.count == 1 && status == :delivered)
@@ -377,7 +380,7 @@ module ActiveMerchant
377
380
  shipment_events[-1] = ShipmentEvent.new(shipment_events.last.name, shipment_events.last.time, destination)
378
381
  end
379
382
  end
380
-
383
+
381
384
  end
382
385
  TrackingResponse.new(success, message, Hash.from_xml(response).values.first,
383
386
  :carrier => @@name,
@@ -395,7 +398,7 @@ module ActiveMerchant
395
398
  :destination => destination,
396
399
  :tracking_number => tracking_number)
397
400
  end
398
-
401
+
399
402
  def location_from_address_node(address)
400
403
  return nil unless address
401
404
  Location.new(
@@ -408,7 +411,7 @@ module ActiveMerchant
408
411
  :address3 => node_text_or_nil(address.elements['AddressLine3'])
409
412
  )
410
413
  end
411
-
414
+
412
415
  def parse_ups_datetime(options = {})
413
416
  time, date = options[:time].to_s, options[:date].to_s
414
417
  if time.nil?
@@ -424,29 +427,29 @@ module ActiveMerchant
424
427
  def response_success?(xml)
425
428
  xml.get_text('/*/Response/ResponseStatusCode').to_s == '1'
426
429
  end
427
-
430
+
428
431
  def response_message(xml)
429
432
  xml.get_text('/*/Response/Error/ErrorDescription | /*/Response/ResponseStatusDescription').to_s
430
433
  end
431
-
434
+
432
435
  def commit(action, request, test = false)
433
436
  ssl_post("#{test ? TEST_URL : LIVE_URL}/#{RESOURCES[action]}", request)
434
437
  end
435
-
436
-
438
+
439
+
437
440
  def service_name_for(origin, code)
438
441
  origin = origin.country_code(:alpha2)
439
-
442
+
440
443
  name = case origin
441
444
  when "CA" then CANADA_ORIGIN_SERVICES[code]
442
445
  when "MX" then MEXICO_ORIGIN_SERVICES[code]
443
446
  when *EU_COUNTRY_CODES then EU_ORIGIN_SERVICES[code]
444
447
  end
445
-
448
+
446
449
  name ||= OTHER_NON_US_ORIGIN_SERVICES[code] unless name == 'US'
447
450
  name ||= DEFAULT_SERVICES[code]
448
451
  end
449
-
452
+
450
453
  end
451
454
  end
452
455
  end