active_shipping 0.9.15 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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