active_shipping 0.12.5 → 0.12.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1976e71d35364954a7b796b394000fa996d3e0b1
4
- data.tar.gz: 0c143eefaaf19b10cfaba63e01110164024b3bae
3
+ metadata.gz: 715dd60ad9d4a6fbe5c9ba36834326fa383c4ee4
4
+ data.tar.gz: 1f1991bf39492b70a068e92e52bb687f4b3526b4
5
5
  SHA512:
6
- metadata.gz: 312abbbb1fd246d4f6221cbf4fa7f70101465d118ff18bc2c588be8f276904e90ee9a9666d90b745a3d29c6abe1cc615230f188e7a6acb759374977a88ce2838
7
- data.tar.gz: db9d8dccf1c806d521188f99fff5b72eb7704da60359bb4239fa293e21be21e646373a0d42fd5df901b597e224bcb5ccd7d7a0c617607c2375242456df1a8843
6
+ metadata.gz: 725e84ee191ce38e56e89a3af8ff0592c21a5e6bc6f584edf9cfdcc4fbb01a5532bc999ae28dfc07045fa403cb8f86c363b8d62e12441fc488efe0e863c33645
7
+ data.tar.gz: db8156ebcbd65a863f222b7bdfb26b37ac3fbd5a6b4e9036fc499488133a7075ec9906f996cafdce5b4ccc002b2e08fabdeffaabf59c82c7da26430ce4519a6a
@@ -264,7 +264,7 @@ module ActiveMerchant
264
264
  end
265
265
 
266
266
  def world_rates(origin, destination, packages, options = {})
267
- request = build_world_rate_request(packages, destination, options)
267
+ request = build_world_rate_request(origin, packages, destination, options)
268
268
  # never use test mode; rate requests just won't work on test servers
269
269
  parse_rate_response origin, destination, packages, commit(:world_rates, request, false), options
270
270
  end
@@ -343,9 +343,10 @@ module ActiveMerchant
343
343
  #
344
344
  # package.options[:mail_type] -- one of [:package, :postcard, :matter_for_the_blind, :envelope].
345
345
  # Defaults to :package.
346
- def build_world_rate_request(packages, destination, options)
346
+ def build_world_rate_request(origin, packages, destination, options)
347
347
  country = COUNTRY_NAME_CONVERSIONS[destination.country.code(:alpha2).value] || destination.country.name
348
348
  request = XmlNode.new('IntlRateV2Request', :USERID => @options[:login]) do |rate_request|
349
+ rate_request << XmlNode.new('Revision', 2)
349
350
  packages.each_index do |id|
350
351
  p = packages[id]
351
352
  rate_request << XmlNode.new('Package', :ID => id.to_s) do |package|
@@ -374,6 +375,9 @@ module ActiveMerchant
374
375
  if commercial_type = commercial_type(options)
375
376
  package << XmlNode.new(COMMERCIAL_FLAG_NAME.fetch(commercial_type), 'Y')
376
377
  end
378
+ package << XmlNode.new('OriginZip', origin.zip)
379
+ package << XmlNode.new('AcceptanceDateTime', (options[:acceptance_time] || Time.now.utc).iso8601)
380
+ package << XmlNode.new('DestinationPostalCode', destination.zip)
377
381
  end
378
382
  end
379
383
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveShipping
2
- VERSION = "0.12.5"
2
+ VERSION = "0.12.6"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_shipping
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.5
4
+ version: 0.12.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - James MacAulay
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2014-12-16 00:00:00.000000000 Z
14
+ date: 2015-06-03 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activesupport
@@ -177,9 +177,7 @@ files:
177
177
  - lib/active_shipping/shipping/carriers/shipwire.rb
178
178
  - lib/active_shipping/shipping/carriers/stamps.rb
179
179
  - lib/active_shipping/shipping/carriers/ups.rb
180
- - lib/active_shipping/shipping/carriers/ups.rb.orig
181
180
  - lib/active_shipping/shipping/carriers/usps.rb
182
- - lib/active_shipping/shipping/carriers/usps.rb.orig
183
181
  - lib/active_shipping/shipping/errors.rb
184
182
  - lib/active_shipping/shipping/label_response.rb
185
183
  - lib/active_shipping/shipping/location.rb
@@ -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
@@ -1,616 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- require 'cgi'
3
-
4
- module ActiveMerchant
5
- module Shipping
6
-
7
- # After getting an API login from USPS (looks like '123YOURNAME456'),
8
- # run the following test:
9
- #
10
- # usps = USPS.new(:login => '123YOURNAME456', :test => true)
11
- # usps.valid_credentials?
12
- #
13
- # This will send a test request to the USPS test servers, which they ask you
14
- # to do before they put your API key in production mode.
15
- class USPS < Carrier
16
- self.retry_safe = true
17
-
18
- cattr_reader :name
19
- @@name = "USPS"
20
-
21
- LIVE_DOMAIN = 'production.shippingapis.com'
22
- LIVE_RESOURCE = 'ShippingAPI.dll'
23
-
24
- TEST_DOMAINS = { #indexed by security; e.g. TEST_DOMAINS[USE_SSL[:rates]]
25
- true => 'secure.shippingapis.com',
26
- false => 'testing.shippingapis.com'
27
- }
28
-
29
- TEST_RESOURCE = 'ShippingAPITest.dll'
30
-
31
- API_CODES = {
32
- :us_rates => 'RateV4',
33
- :world_rates => 'IntlRateV2',
34
- :test => 'CarrierPickupAvailability',
35
- :track => 'TrackV2'
36
- }
37
- USE_SSL = {
38
- :us_rates => false,
39
- :world_rates => false,
40
- :test => true,
41
- :track => false
42
- }
43
- CONTAINERS = {
44
- :envelope => 'Flat Rate Envelope',
45
- :box => 'Flat Rate Box'
46
- }
47
- MAIL_TYPES = {
48
- :package => 'Package',
49
- :postcard => 'Postcards or aerogrammes',
50
- :matter_for_the_blind => 'Matter for the blind',
51
- :envelope => 'Envelope'
52
- }
53
-
54
- PACKAGE_PROPERTIES = {
55
- 'ZipOrigination' => :origin_zip,
56
- 'ZipDestination' => :destination_zip,
57
- 'Pounds' => :pounds,
58
- 'Ounces' => :ounces,
59
- 'Container' => :container,
60
- 'Size' => :size,
61
- 'Machinable' => :machinable,
62
- 'Zone' => :zone,
63
- 'Postage' => :postage,
64
- 'Restrictions' => :restrictions
65
- }
66
- POSTAGE_PROPERTIES = {
67
- 'MailService' => :service,
68
- 'Rate' => :rate
69
- }
70
- US_SERVICES = {
71
- :first_class => 'FIRST CLASS',
72
- :priority => 'PRIORITY',
73
- :express => 'EXPRESS',
74
- :bpm => 'BPM',
75
- :parcel => 'PARCEL',
76
- :media => 'MEDIA',
77
- :library => 'LIBRARY',
78
- :all => 'ALL'
79
- }
80
- FIRST_CLASS_MAIL_TYPES = {
81
- :letter => 'LETTER',
82
- :flat => 'FLAT',
83
- :parcel => 'PARCEL',
84
- :post_card => 'POSTCARD',
85
- :package_service => 'PACKAGESERVICE'
86
- }
87
-
88
- # TODO: get rates for "U.S. possessions and Trust Territories" like Guam, etc. via domestic rates API: http://www.usps.com/ncsc/lookups/abbr_state.txt
89
- # TODO: figure out how USPS likes to say "Ivory Coast"
90
- #
91
- # Country names:
92
- # http://pe.usps.gov/text/Imm/immctry.htm
93
- COUNTRY_NAME_CONVERSIONS = {
94
- "BA" => "Bosnia-Herzegovina",
95
- "CD" => "Congo, Democratic Republic of the",
96
- "CG" => "Congo (Brazzaville),Republic of the",
97
- "CI" => "Côte d'Ivoire (Ivory Coast)",
98
- "CK" => "Cook Islands (New Zealand)",
99
- "FK" => "Falkland Islands",
100
- "GB" => "Great Britain and Northern Ireland",
101
- "GE" => "Georgia, Republic of",
102
- "IR" => "Iran",
103
- "KN" => "Saint Kitts (St. Christopher and Nevis)",
104
- "KP" => "North Korea (Korea, Democratic People's Republic of)",
105
- "KR" => "South Korea (Korea, Republic of)",
106
- "LA" => "Laos",
107
- "LY" => "Libya",
108
- "MC" => "Monaco (France)",
109
- "MD" => "Moldova",
110
- "MK" => "Macedonia, Republic of",
111
- "MM" => "Burma",
112
- "PN" => "Pitcairn Island",
113
- "RU" => "Russia",
114
- "SK" => "Slovak Republic",
115
- "TK" => "Tokelau (Union) Group (Western Samoa)",
116
- "TW" => "Taiwan",
117
- "TZ" => "Tanzania",
118
- "VA" => "Vatican City",
119
- "VG" => "British Virgin Islands",
120
- "VN" => "Vietnam",
121
- "WF" => "Wallis and Futuna Islands",
122
- "WS" => "Western Samoa"
123
- }
124
-
125
- def find_tracking_info(tracking_number, options={})
126
- options = @options.update(options)
127
- tracking_request = build_tracking_request(tracking_number, options)
128
- response = commit(:track, tracking_request, (options[:test] || false))
129
- parse_tracking_response(response, options)
130
- end
131
-
132
- def self.size_code_for(package)
133
- if package.inches(:max) <= 12
134
- 'REGULAR'
135
- else
136
- 'LARGE'
137
- end
138
- end
139
-
140
- # from info at http://www.usps.com/businessmail101/mailcharacteristics/parcels.htm
141
- #
142
- # package.options[:books] -- 25 lb. limit instead of 35 for books or other printed matter.
143
- # Defaults to false.
144
- def self.package_machinable?(package, options={})
145
- at_least_minimum = package.inches(:length) >= 6.0 &&
146
- package.inches(:width) >= 3.0 &&
147
- package.inches(:height) >= 0.25 &&
148
- package.ounces >= 6.0
149
- at_most_maximum = package.inches(:length) <= 34.0 &&
150
- package.inches(:width) <= 17.0 &&
151
- package.inches(:height) <= 17.0 &&
152
- package.pounds <= (package.options[:books] ? 25.0 : 35.0)
153
- at_least_minimum && at_most_maximum
154
- end
155
-
156
- def requirements
157
- [:login]
158
- end
159
-
160
- def find_rates(origin, destination, packages, options = {})
161
- options = @options.merge(options)
162
-
163
- origin = Location.from(origin)
164
- destination = Location.from(destination)
165
- packages = Array(packages)
166
-
167
- #raise ArgumentError.new("USPS packages must originate in the U.S.") unless ['US',nil].include?(origin.country_code(:alpha2))
168
-
169
-
170
- # domestic or international?
171
-
172
- response = if ['US',nil].include?(destination.country_code(:alpha2))
173
- us_rates(origin, destination, packages, options)
174
- else
175
- world_rates(origin, destination, packages, options)
176
- end
177
- end
178
-
179
- def valid_credentials?
180
- # Cannot test with find_rates because USPS doesn't allow that in test mode
181
- test_mode? ? canned_address_verification_works? : super
182
- end
183
-
184
- def maximum_weight
185
- Mass.new(70, :pounds)
186
- end
187
- <<<<<<< HEAD
188
-
189
-
190
- def find_tracking_info(tracking_number, options={})
191
- options = @options.update(options)
192
- tracking_request = build_tracking_request(tracking_number, options)
193
- response = commit(save_request(tracking_request), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
194
- parse_tracking_response(response, options)
195
- end
196
-
197
-
198
- =======
199
-
200
- >>>>>>> d1ef4f7... further cleanup prior to pull request
201
- protected
202
-
203
-
204
-
205
- def response_success?(xml)
206
- xml.get_text('/*/Response/ResponseStatusCode').to_s == '1'
207
- end
208
-
209
- def response_message(xml)
210
- xml.get_text('/*/Response/Error/ErrorDescription | /*/Response/ResponseStatusDescription').to_s
211
- end
212
-
213
- def parse_tracking_response(response, options={})
214
- xml = REXML::Document.new(response)
215
- success = response_success?(xml)
216
- message = response_message(xml)
217
-
218
- if success
219
- tracking_number, origin, destination = nil
220
- shipment_events = []
221
-
222
- first_shipment = xml.elements['/*/Shipment']
223
- first_package = first_shipment.elements['Package']
224
- tracking_number = first_shipment.get_text('ShipmentIdentificationNumber | Package/TrackingNumber').to_s
225
-
226
- origin, destination = %w{Shipper ShipTo}.map do |location|
227
- location_from_address_node(first_shipment.elements["#{location}/Address"])
228
- end
229
-
230
- activities = first_package.get_elements('Activity')
231
- unless activities.empty?
232
- shipment_events = activities.map do |activity|
233
- description = activity.get_text('Status/StatusType/Description').to_s
234
- zoneless_time = if (time = activity.get_text('Time')) &&
235
- (date = activity.get_text('Date'))
236
- time, date = time.to_s, date.to_s
237
- hour, minute, second = time.scan(/\d{2}/)
238
- year, month, day = date[0..3], date[4..5], date[6..7]
239
- Time.utc(year, month, day, hour, minute, second)
240
- end
241
- location = location_from_address_node(activity.elements['ActivityLocation/Address'])
242
- ShipmentEvent.new(description, zoneless_time, location)
243
- end
244
-
245
- shipment_events = shipment_events.sort_by(&:time)
246
-
247
- if origin
248
- first_event = shipment_events[0]
249
- same_country = origin.country_code(:alpha2) == first_event.location.country_code(:alpha2)
250
- same_or_blank_city = first_event.location.city.blank? or first_event.location.city == origin.city
251
- origin_event = ShipmentEvent.new(first_event.name, first_event.time, origin)
252
- if same_country and same_or_blank_city
253
- shipment_events[0] = origin_event
254
- else
255
- shipment_events.unshift(origin_event)
256
- end
257
- end
258
- if shipment_events.last.name.downcase == 'delivered'
259
- shipment_events[-1] = ShipmentEvent.new(shipment_events.last.name, shipment_events.last.time, destination)
260
- end
261
- end
262
-
263
- end
264
- TrackingResponse.new(success, message, Hash.from_xml(response).values.first,
265
- :xml => response,
266
- :request => last_request,
267
- :shipment_events => shipment_events,
268
- :origin => origin,
269
- :destination => destination,
270
- :tracking_number => tracking_number)
271
- end
272
-
273
-
274
-
275
-
276
-
277
-
278
- def build_tracking_request(tracking_number, options={})
279
- xml_request = XmlNode.new('TrackRequest', 'USERID' => @options[:login]) do |root_node|
280
- root_node << XmlNode.new('TrackID', :ID => tracking_number)
281
- end
282
- URI.encode(xml_request.to_s)
283
- end
284
-
285
- def us_rates(origin, destination, packages, options={})
286
- request = build_us_rate_request(packages, origin.zip, destination.zip, options)
287
- # never use test mode; rate requests just won't work on test servers
288
- parse_rate_response origin, destination, packages, commit(:us_rates,request,false), options
289
- end
290
-
291
- def world_rates(origin, destination, packages, options={})
292
- request = build_world_rate_request(packages, destination)
293
- # never use test mode; rate requests just won't work on test servers
294
- parse_rate_response origin, destination, packages, commit(:world_rates,request,false), options
295
- end
296
-
297
- # Once the address verification API is implemented, remove this and have valid_credentials? build the request using that instead.
298
- def canned_address_verification_works?
299
- 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"
300
- # 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"}}
301
- xml = REXML::Document.new(commit(:test, request, true))
302
- xml.get_text('/CarrierPickupAvailabilityResponse/City').to_s == 'HOUSTON' &&
303
- xml.get_text('/CarrierPickupAvailabilityResponse/Address2').to_s == '1390 Market Street'
304
- end
305
-
306
- # options[:service] -- One of [:first_class, :priority, :express, :bpm, :parcel,
307
- # :media, :library, :all]. defaults to :all.
308
- # options[:container] -- One of [:envelope, :box]. defaults to neither (this field has
309
- # special meaning in the USPS API).
310
- # options[:books] -- Either true or false. Packages of books or other printed matter
311
- # have a lower weight limit to be considered machinable.
312
- # package.options[:machinable] -- Either true or false. Overrides the detection of
313
- # "machinability" entirely.
314
- def build_us_rate_request(packages, origin_zip, destination_zip, options={})
315
- packages = Array(packages)
316
- request = XmlNode.new('RateV4Request', :USERID => @options[:login]) do |rate_request|
317
- packages.each_with_index do |p,id|
318
- rate_request << XmlNode.new('Package', :ID => id.to_s) do |package|
319
- package << XmlNode.new('Service', US_SERVICES[options[:service] || :all])
320
- package << XmlNode.new('FirstClassMailType', FIRST_CLASS_MAIL_TYPES[options[:first_class_mail_type]])
321
- package << XmlNode.new('ZipOrigination', strip_zip(origin_zip))
322
- package << XmlNode.new('ZipDestination', strip_zip(destination_zip))
323
- package << XmlNode.new('Pounds', 0)
324
- package << XmlNode.new('Ounces', "%0.1f" % [p.ounces,1].max)
325
- package << XmlNode.new('Container', CONTAINERS[p.options[:container]])
326
- package << XmlNode.new('Size', USPS.size_code_for(p))
327
- package << XmlNode.new('Width', "%0.2f" % p.inches(:width))
328
- package << XmlNode.new('Length', "%0.2f" % p.inches(:length))
329
- package << XmlNode.new('Height', "%0.2f" % p.inches(:height))
330
- package << XmlNode.new('Girth', "%0.2f" % p.inches(:girth))
331
- is_machinable = if p.options.has_key?(:machinable)
332
- p.options[:machinable] ? true : false
333
- else
334
- USPS.package_machinable?(p)
335
- end
336
- package << XmlNode.new('Machinable', is_machinable.to_s.upcase)
337
- end
338
- end
339
- end
340
- URI.encode(save_request(request.to_s))
341
- end
342
-
343
- # important difference with international rate requests:
344
- # * services are not given in the request
345
- # * package sizes are not given in the request
346
- # * services are returned in the response along with restrictions of size
347
- # * the size restrictions are returned AS AN ENGLISH SENTENCE (!?)
348
- #
349
- #
350
- # package.options[:mail_type] -- one of [:package, :postcard, :matter_for_the_blind, :envelope].
351
- # Defaults to :package.
352
- def build_world_rate_request(packages, destination)
353
- country = COUNTRY_NAME_CONVERSIONS[destination.country.code(:alpha2).value] || destination.country.name
354
- request = XmlNode.new('IntlRateV2Request', :USERID => @options[:login]) do |rate_request|
355
- packages.each_index do |id|
356
- p = packages[id]
357
- rate_request << XmlNode.new('Package', :ID => id.to_s) do |package|
358
- package << XmlNode.new('Pounds', 0)
359
- package << XmlNode.new('Ounces', [p.ounces,1].max.ceil) #takes an integer for some reason, must be rounded UP
360
- package << XmlNode.new('MailType', MAIL_TYPES[p.options[:mail_type]] || 'Package')
361
- package << XmlNode.new('GXG') do |gxg|
362
- gxg << XmlNode.new('POBoxFlag', destination.po_box? ? 'Y' : 'N')
363
- gxg << XmlNode.new('GiftFlag', p.gift? ? 'Y' : 'N')
364
- end
365
- value = if p.value && p.value > 0 && p.currency && p.currency != 'USD'
366
- 0.0
367
- else
368
- (p.value || 0) / 100.0
369
- end
370
- package << XmlNode.new('ValueOfContents', value)
371
- package << XmlNode.new('Country') do |node|
372
- node.cdata = country
373
- end
374
- package << XmlNode.new('Container', p.cylinder? ? 'NONRECTANGULAR' : 'RECTANGULAR')
375
- package << XmlNode.new('Size', USPS.size_code_for(p))
376
- package << XmlNode.new('Width', "%0.2f" % [p.inches(:width), 0.01].max)
377
- package << XmlNode.new('Length', "%0.2f" % [p.inches(:length), 0.01].max)
378
- package << XmlNode.new('Height', "%0.2f" % [p.inches(:height), 0.01].max)
379
- package << XmlNode.new('Girth', "%0.2f" % [p.inches(:girth), 0.01].max)
380
- end
381
- end
382
- end
383
- URI.encode(save_request(request.to_s))
384
- end
385
-
386
- def parse_rate_response(origin, destination, packages, response, options={})
387
- success = true
388
- message = ''
389
- rate_hash = {}
390
-
391
- xml = REXML::Document.new(response)
392
-
393
- if error = xml.elements['/Error']
394
- success = false
395
- message = error.elements['Description'].text
396
- else
397
- xml.elements.each('/*/Package') do |package|
398
- if package.elements['Error']
399
- success = false
400
- message = package.get_text('Error/Description').to_s
401
- break
402
- end
403
- end
404
-
405
- if success
406
- rate_hash = rates_from_response_node(xml, packages)
407
- unless rate_hash
408
- success = false
409
- message = "Unknown root node in XML response: '#{xml.root.name}'"
410
- end
411
- end
412
-
413
- end
414
-
415
- if success
416
- rate_estimates = rate_hash.keys.map do |service_name|
417
- RateEstimate.new(origin,destination,@@name,"USPS #{service_name}",
418
- :package_rates => rate_hash[service_name][:package_rates],
419
- :service_code => rate_hash[service_name][:service_code],
420
- :currency => 'USD')
421
- end
422
- rate_estimates.reject! {|e| e.package_count != packages.length}
423
- rate_estimates = rate_estimates.sort_by(&:total_price)
424
- end
425
-
426
- RateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request)
427
- end
428
-
429
- def rates_from_response_node(response_node, packages)
430
- rate_hash = {}
431
- return false unless (root_node = response_node.elements['/IntlRateV2Response | /RateV4Response'])
432
- domestic = (root_node.name == 'RateV4Response')
433
-
434
- domestic_elements = ['Postage', 'CLASSID', 'MailService', 'Rate']
435
- international_elements = ['Service', 'ID', 'SvcDescription', 'Postage']
436
- service_node, service_code_node, service_name_node, rate_node = domestic ? domestic_elements : international_elements
437
-
438
- root_node.each_element('Package') do |package_node|
439
- this_package = packages[package_node.attributes['ID'].to_i]
440
-
441
- package_node.each_element(service_node) do |service_response_node|
442
- service_name = service_response_node.get_text(service_name_node).to_s
443
-
444
- # strips the double-escaped HTML for trademark symbols from service names
445
- service_name.gsub!(/&amp;lt;\S*&amp;gt;/,'')
446
- # ...leading "USPS"
447
- service_name.gsub!(/^USPS/,'')
448
- # ...trailing asterisks
449
- service_name.gsub!(/\*+$/,'')
450
- # ...surrounding spaces
451
- service_name.strip!
452
-
453
- # aggregate specific package rates into a service-centric RateEstimate
454
- # first package with a given service name will initialize these;
455
- # later packages with same service will add to them
456
- this_service = rate_hash[service_name] ||= {}
457
- this_service[:service_code] ||= service_response_node.attributes[service_code_node]
458
- package_rates = this_service[:package_rates] ||= []
459
- this_package_rate = {:package => this_package,
460
- :rate => Package.cents_from(service_response_node.get_text(rate_node).to_s.to_f)}
461
-
462
- package_rates << this_package_rate if package_valid_for_service(this_package,service_response_node)
463
- end
464
- end
465
- rate_hash
466
- end
467
-
468
- def package_valid_for_service(package, service_node)
469
- return true if service_node.elements['MaxWeight'].nil?
470
- max_weight = service_node.get_text('MaxWeight').to_s.to_f
471
- name = service_node.get_text('SvcDescription | MailService').to_s.downcase
472
-
473
- if name =~ /flat.rate.box/ #domestic or international flat rate box
474
- # flat rate dimensions from http://www.usps.com/shipping/flatrate.htm
475
- return (package_valid_for_max_dimensions(package,
476
- :weight => max_weight, #domestic apparently has no weight restriction
477
- :length => 11.0,
478
- :width => 8.5,
479
- :height => 5.5) or
480
- package_valid_for_max_dimensions(package,
481
- :weight => max_weight,
482
- :length => 13.625,
483
- :width => 11.875,
484
- :height => 3.375))
485
- elsif name =~ /flat.rate.envelope/
486
- return package_valid_for_max_dimensions(package,
487
- :weight => max_weight,
488
- :length => 12.5,
489
- :width => 9.5,
490
- :height => 0.75)
491
- elsif service_node.elements['MailService'] # domestic non-flat rates
492
- return true
493
- else #international non-flat rates
494
- # Some sample english that this is required to parse:
495
- #
496
- # 'Max. length 46", width 35", height 46" and max. length plus girth 108"'
497
- # 'Max. length 24", Max. length, height, depth combined 36"'
498
- #
499
- sentence = CGI.unescapeHTML(service_node.get_text('MaxDimensions').to_s)
500
- tokens = sentence.downcase.split(/[^\d]*"/).reject {|t| t.empty?}
501
- max_dimensions = {:weight => max_weight}
502
- single_axis_values = []
503
- tokens.each do |token|
504
- axis_sum = [/length/,/width/,/height/,/depth/].sum {|regex| (token =~ regex) ? 1 : 0}
505
- unless axis_sum == 0
506
- value = token[/\d+$/].to_f
507
- if axis_sum == 3
508
- max_dimensions[:length_plus_width_plus_height] = value
509
- elsif token =~ /girth/ and axis_sum == 1
510
- max_dimensions[:length_plus_girth] = value
511
- else
512
- single_axis_values << value
513
- end
514
- end
515
- end
516
- single_axis_values.sort!.reverse!
517
- [:length, :width, :height].each_with_index do |axis,i|
518
- max_dimensions[axis] = single_axis_values[i] if single_axis_values[i]
519
- end
520
- return package_valid_for_max_dimensions(package, max_dimensions)
521
- end
522
- end
523
-
524
- def package_valid_for_max_dimensions(package,dimensions)
525
- valid = ((not ([:length,:width,:height].map {|dim| dimensions[dim].nil? || dimensions[dim].to_f >= package.inches(dim).to_f}.include?(false))) and
526
- (dimensions[:weight].nil? || dimensions[:weight] >= package.pounds) and
527
- (dimensions[:length_plus_girth].nil? or
528
- dimensions[:length_plus_girth].to_f >=
529
- package.inches(:length) + package.inches(:girth)) and
530
- (dimensions[:length_plus_width_plus_height].nil? or
531
- dimensions[:length_plus_width_plus_height].to_f >=
532
- package.inches(:length) + package.inches(:width) + package.inches(:height)))
533
-
534
- return valid
535
- end
536
-
537
- def parse_tracking_response(response, options)
538
- xml = REXML::Document.new(response)
539
- root_node = xml.elements['TrackResponse']
540
-
541
- success = response_success?(xml)
542
- message = response_message(xml)
543
-
544
- if success
545
- tracking_number, origin, destination = nil
546
- shipment_events = []
547
- tracking_details = xml.elements.collect('*/*/TrackDetail'){ |e| e }
548
-
549
- tracking_summary = xml.elements.collect('*/*/TrackSummary'){ |e| e }.first
550
- tracking_details << tracking_summary
551
-
552
- tracking_number = root_node.elements['TrackInfo'].attributes['ID'].to_s
553
-
554
- tracking_details.each do |event|
555
- location = nil
556
- timestamp = nil
557
- description = nil
558
- if event.get_text.to_s =~ /^(.*), (\w+ \d\d, \d{4}, \d{1,2}:\d\d [ap]m), (.*), (\w\w) (\d{5})$/i ||
559
- event.get_text.to_s =~ /^Your item \w{2,3} (out for delivery|delivered) at (\d{1,2}:\d\d [ap]m on \w+ \d\d, \d{4}) in (.*), (\w\w) (\d{5})\.$/i
560
- description = $1.upcase
561
- timestamp = $2
562
- city = $3
563
- state = $4
564
- zip_code = $5
565
- location = Location.new(:city => city, :state => state, :postal_code => zip_code, :country => 'USA')
566
- end
567
- if location
568
- time = Time.parse(timestamp)
569
- zoneless_time = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec)
570
- shipment_events << ShipmentEvent.new(description, zoneless_time, location)
571
- end
572
- end
573
- shipment_events = shipment_events.sort_by(&:time)
574
- end
575
-
576
- TrackingResponse.new(success, message, Hash.from_xml(response),
577
- :xml => response,
578
- :request => last_request,
579
- :shipment_events => shipment_events,
580
- :destination => destination,
581
- :tracking_number => tracking_number
582
- )
583
- end
584
-
585
- def response_status_node(document)
586
- document.elements['*/*/TrackSummary'] || document.elements['Error/Description']
587
- end
588
-
589
- def response_success?(document)
590
- summary = response_status_node(document).get_text.to_s
591
- !(summary =~ /There is no record of that mail item/ || summary =~ /This Information has not been included in this Test Server\./)
592
- end
593
-
594
- def response_message(document)
595
- response_node = response_status_node(document)
596
- response_status_node(document).get_text.to_s
597
- end
598
-
599
- def commit(action, request, test = false)
600
- ssl_get(request_url(action, request, test))
601
- end
602
-
603
- def request_url(action, request, test)
604
- scheme = USE_SSL[action] ? 'https://' : 'http://'
605
- host = test ? TEST_DOMAINS[USE_SSL[action]] : LIVE_DOMAIN
606
- resource = test ? TEST_RESOURCE : LIVE_RESOURCE
607
- "#{scheme}#{host}/#{resource}?API=#{API_CODES[action]}&XML=#{request}"
608
- end
609
-
610
- def strip_zip(zip)
611
- zip.to_s.scan(/\d{5}/).first || zip
612
- end
613
-
614
- end
615
- end
616
- end