active_shipping 0.9.14 → 0.9.15
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.
- data/MIT-LICENSE +2 -2
- data/README.markdown +2 -0
- data/lib/active_shipping/shipping/carrier.rb +11 -0
- data/lib/active_shipping/shipping/carriers/fedex.rb +135 -12
- data/lib/active_shipping/shipping/carriers/shipwire.rb +1 -11
- data/lib/active_shipping/shipping/carriers/ups.rb +68 -10
- data/lib/active_shipping/shipping/location.rb +12 -0
- data/lib/active_shipping/shipping/tracking_response.rb +26 -1
- data/lib/active_shipping/version.rb +1 -1
- metadata +135 -131
data/MIT-LICENSE
CHANGED
|
@@ -13,8 +13,8 @@ included in all copies or substantial portions of the Software.
|
|
|
13
13
|
|
|
14
14
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
15
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
17
|
-
NONINFRINGEMENT. IN NO EVENT
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
18
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
19
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
20
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Active Shipping
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
This library interfaces with the web services of various shipping carriers. The goal is to abstract the features that are most frequently used into a pleasant and consistent Ruby API. Active Shipping is an extension of [Active Merchant][], and as such, it borrows heavily from conventions used in the latter.
|
|
4
6
|
|
|
5
7
|
Active Shipping is currently being used and improved in a production environment for [Shopify][]. Development is being done by the Shopify integrations team (<integrations-team@shopify.com>). Discussion is welcome in the [Active Merchant Google Group][discuss].
|
|
@@ -65,6 +65,17 @@ module ActiveMerchant
|
|
|
65
65
|
def save_request(r)
|
|
66
66
|
@last_request = r
|
|
67
67
|
end
|
|
68
|
+
|
|
69
|
+
def timestamp_from_business_day(days)
|
|
70
|
+
return unless days
|
|
71
|
+
date = DateTime.now.utc
|
|
72
|
+
days.times do
|
|
73
|
+
begin
|
|
74
|
+
date = date + 1
|
|
75
|
+
end until ![0,6].include?(date.wday)
|
|
76
|
+
end
|
|
77
|
+
date
|
|
78
|
+
end
|
|
68
79
|
end
|
|
69
80
|
end
|
|
70
81
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# FedEx module by Jimmy Baker
|
|
2
2
|
# http://github.com/jimmyebaker
|
|
3
3
|
|
|
4
|
+
require 'date'
|
|
4
5
|
module ActiveMerchant
|
|
5
6
|
module Shipping
|
|
6
7
|
|
|
@@ -85,6 +86,45 @@ module ActiveMerchant
|
|
|
85
86
|
'express_mps_master' => 'EXPRESS_MPS_MASTER'
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
|
|
90
|
+
TransitTimes = ["UNKNOWN","ONE_DAY","TWO_DAYS","THREE_DAYS","FOUR_DAYS","FIVE_DAYS","SIX_DAYS","SEVEN_DAYS","EIGHT_DAYS","NINE_DAYS","TEN_DAYS","ELEVEN_DAYS","TWELVE_DAYS","THIRTEEN_DAYS","FOURTEEN_DAYS","FIFTEEN_DAYS","SIXTEEN_DAYS","SEVENTEEN_DAYS","EIGHTEEN_DAYS"]
|
|
91
|
+
|
|
92
|
+
# FedEx tracking codes as described in the FedEx Tracking Service WSDL Guide
|
|
93
|
+
# All delays also have been marked as exceptions
|
|
94
|
+
TRACKING_STATUS_CODES = HashWithIndifferentAccess.new({
|
|
95
|
+
'AA' => :at_airport,
|
|
96
|
+
'AD' => :at_delivery,
|
|
97
|
+
'AF' => :at_fedex_facility,
|
|
98
|
+
'AR' => :at_fedex_facility,
|
|
99
|
+
'AP' => :at_pickup,
|
|
100
|
+
'CA' => :canceled,
|
|
101
|
+
'CH' => :location_changed,
|
|
102
|
+
'DE' => :exception,
|
|
103
|
+
'DL' => :delivered,
|
|
104
|
+
'DP' => :departed_fedex_location,
|
|
105
|
+
'DR' => :vehicle_furnished_not_used,
|
|
106
|
+
'DS' => :vehicle_dispatched,
|
|
107
|
+
'DY' => :exception,
|
|
108
|
+
'EA' => :exception,
|
|
109
|
+
'ED' => :enroute_to_delivery,
|
|
110
|
+
'EO' => :enroute_to_origin_airport,
|
|
111
|
+
'EP' => :enroute_to_pickup,
|
|
112
|
+
'FD' => :at_fedex_destination,
|
|
113
|
+
'HL' => :held_at_location,
|
|
114
|
+
'IT' => :in_transit,
|
|
115
|
+
'LO' => :left_origin,
|
|
116
|
+
'OC' => :order_created,
|
|
117
|
+
'OD' => :out_for_delivery,
|
|
118
|
+
'PF' => :plane_in_flight,
|
|
119
|
+
'PL' => :plane_landed,
|
|
120
|
+
'PU' => :picked_up,
|
|
121
|
+
'RS' => :return_to_shipper,
|
|
122
|
+
'SE' => :exception,
|
|
123
|
+
'SF' => :at_sort_facility,
|
|
124
|
+
'SP' => :split_status,
|
|
125
|
+
'TR' => :transfer
|
|
126
|
+
})
|
|
127
|
+
|
|
88
128
|
def self.service_name_for_code(service_code)
|
|
89
129
|
ServiceTypes[service_code] || "FedEx #{service_code.titleize.sub(/Fedex /, '')}"
|
|
90
130
|
end
|
|
@@ -100,7 +140,7 @@ module ActiveMerchant
|
|
|
100
140
|
rate_request = build_rate_request(origin, destination, packages, options)
|
|
101
141
|
|
|
102
142
|
response = commit(save_request(rate_request), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
|
|
103
|
-
|
|
143
|
+
|
|
104
144
|
parse_rate_response(origin, destination, packages, response, options)
|
|
105
145
|
end
|
|
106
146
|
|
|
@@ -111,7 +151,7 @@ module ActiveMerchant
|
|
|
111
151
|
response = commit(save_request(tracking_request), (options[:test] || false)).gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
|
|
112
152
|
parse_tracking_response(response, options)
|
|
113
153
|
end
|
|
114
|
-
|
|
154
|
+
|
|
115
155
|
protected
|
|
116
156
|
def build_rate_request(origin, destination, packages, options={})
|
|
117
157
|
imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))
|
|
@@ -133,7 +173,7 @@ module ActiveMerchant
|
|
|
133
173
|
root_node << XmlNode.new('VariableOptions', 'SATURDAY_DELIVERY')
|
|
134
174
|
|
|
135
175
|
root_node << XmlNode.new('RequestedShipment') do |rs|
|
|
136
|
-
rs << XmlNode.new('ShipTimestamp',
|
|
176
|
+
rs << XmlNode.new('ShipTimestamp', ship_timestamp(options[:turn_around_time]))
|
|
137
177
|
rs << XmlNode.new('DropoffType', options[:dropoff_type] || 'REGULAR_PICKUP')
|
|
138
178
|
rs << XmlNode.new('PackagingType', options[:packaging_type] || 'YOUR_PACKAGING')
|
|
139
179
|
|
|
@@ -236,15 +276,22 @@ module ActiveMerchant
|
|
|
236
276
|
is_saturday_delivery = rated_shipment.get_text('AppliedOptions').to_s == 'SATURDAY_DELIVERY'
|
|
237
277
|
service_type = is_saturday_delivery ? "#{service_code}_SATURDAY_DELIVERY" : service_code
|
|
238
278
|
|
|
239
|
-
|
|
279
|
+
transit_time = rated_shipment.get_text('TransitTime').to_s if service_code == "FEDEX_GROUND"
|
|
280
|
+
max_transit_time = rated_shipment.get_text('MaximumTransitTime').to_s if service_code == "FEDEX_GROUND"
|
|
281
|
+
|
|
282
|
+
delivery_timestamp = rated_shipment.get_text('DeliveryTimestamp').to_s
|
|
283
|
+
|
|
284
|
+
delivery_range = delivery_range_from(transit_time, max_transit_time, delivery_timestamp, options)
|
|
285
|
+
|
|
286
|
+
currency = handle_incorrect_currency_codes(rated_shipment.get_text('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Currency').to_s)
|
|
240
287
|
rate_estimates << RateEstimate.new(origin, destination, @@name,
|
|
241
288
|
self.class.service_name_for_code(service_type),
|
|
242
289
|
:service_code => service_code,
|
|
243
290
|
:total_price => rated_shipment.get_text('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Amount').to_s.to_f,
|
|
244
291
|
:currency => currency,
|
|
245
292
|
:packages => packages,
|
|
246
|
-
:delivery_range =>
|
|
247
|
-
|
|
293
|
+
:delivery_range => delivery_range)
|
|
294
|
+
end
|
|
248
295
|
|
|
249
296
|
if rate_estimates.empty?
|
|
250
297
|
success = false
|
|
@@ -253,7 +300,35 @@ module ActiveMerchant
|
|
|
253
300
|
|
|
254
301
|
RateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request, :log_xml => options[:log_xml])
|
|
255
302
|
end
|
|
256
|
-
|
|
303
|
+
|
|
304
|
+
def delivery_range_from(transit_time, max_transit_time, delivery_timestamp, options)
|
|
305
|
+
delivery_range = [delivery_timestamp, delivery_timestamp]
|
|
306
|
+
|
|
307
|
+
#if there's no delivery timestamp but we do have a transit time, use it
|
|
308
|
+
if delivery_timestamp.blank? && transit_time.present?
|
|
309
|
+
transit_range = parse_transit_times([transit_time,max_transit_time.presence || transit_time])
|
|
310
|
+
delivery_range = transit_range.map{|days| business_days_from(ship_date(options[:turn_around_time]), days)}
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
delivery_range
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def business_days_from(date, days)
|
|
317
|
+
future_date = date
|
|
318
|
+
count = 0
|
|
319
|
+
|
|
320
|
+
while count < days
|
|
321
|
+
future_date += 1.day
|
|
322
|
+
count += 1 if business_day?(future_date)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
future_date
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def business_day?(date)
|
|
329
|
+
(1..5).include?(date.wday)
|
|
330
|
+
end
|
|
331
|
+
|
|
257
332
|
def parse_tracking_response(response, options)
|
|
258
333
|
xml = REXML::Document.new(response)
|
|
259
334
|
root_node = xml.elements['TrackReply']
|
|
@@ -262,13 +337,32 @@ module ActiveMerchant
|
|
|
262
337
|
message = response_message(xml)
|
|
263
338
|
|
|
264
339
|
if success
|
|
265
|
-
tracking_number, origin, destination = nil
|
|
340
|
+
tracking_number, origin, destination, status, status_code, status_description = nil
|
|
266
341
|
shipment_events = []
|
|
267
|
-
|
|
342
|
+
|
|
268
343
|
tracking_details = root_node.elements['TrackDetails']
|
|
269
344
|
tracking_number = tracking_details.get_text('TrackingNumber').to_s
|
|
270
345
|
|
|
346
|
+
status_code = tracking_details.get_text('StatusCode').to_s
|
|
347
|
+
status_description = tracking_details.get_text('StatusDescription').to_s
|
|
348
|
+
status = TRACKING_STATUS_CODES[status_code]
|
|
349
|
+
|
|
350
|
+
origin_node = tracking_details.elements['OriginLocationAddress']
|
|
351
|
+
|
|
352
|
+
if origin_node
|
|
353
|
+
origin = Location.new(
|
|
354
|
+
:country => origin_node.get_text('CountryCode').to_s,
|
|
355
|
+
:province => origin_node.get_text('StateOrProvinceCode').to_s,
|
|
356
|
+
:city => origin_node.get_text('City').to_s
|
|
357
|
+
)
|
|
358
|
+
end
|
|
359
|
+
|
|
271
360
|
destination_node = tracking_details.elements['DestinationAddress']
|
|
361
|
+
|
|
362
|
+
if destination_node.nil?
|
|
363
|
+
destination_node = tracking_details.elements['ActualDeliveryAddress']
|
|
364
|
+
end
|
|
365
|
+
|
|
272
366
|
destination = Location.new(
|
|
273
367
|
:country => destination_node.get_text('CountryCode').to_s,
|
|
274
368
|
:province => destination_node.get_text('StateOrProvinceCode').to_s,
|
|
@@ -294,17 +388,33 @@ module ActiveMerchant
|
|
|
294
388
|
shipment_events << ShipmentEvent.new(description, zoneless_time, location)
|
|
295
389
|
end
|
|
296
390
|
shipment_events = shipment_events.sort_by(&:time)
|
|
391
|
+
|
|
297
392
|
end
|
|
298
393
|
|
|
299
394
|
TrackingResponse.new(success, message, Hash.from_xml(response),
|
|
395
|
+
:carrier => @@name,
|
|
300
396
|
:xml => response,
|
|
301
397
|
:request => last_request,
|
|
398
|
+
:status => status,
|
|
399
|
+
:status_code => status_code,
|
|
400
|
+
:status_description => status_description,
|
|
302
401
|
:shipment_events => shipment_events,
|
|
402
|
+
:origin => origin,
|
|
303
403
|
:destination => destination,
|
|
304
404
|
:tracking_number => tracking_number
|
|
305
405
|
)
|
|
306
406
|
end
|
|
307
|
-
|
|
407
|
+
|
|
408
|
+
def ship_timestamp(delay_in_hours)
|
|
409
|
+
delay_in_hours ||= 0
|
|
410
|
+
Time.now + delay_in_hours.hours
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def ship_date(delay_in_hours)
|
|
414
|
+
delay_in_hours ||= 0
|
|
415
|
+
(Time.now + delay_in_hours.hours).to_date
|
|
416
|
+
end
|
|
417
|
+
|
|
308
418
|
def response_status_node(document)
|
|
309
419
|
document.elements['/*/Notifications/']
|
|
310
420
|
end
|
|
@@ -322,8 +432,21 @@ module ActiveMerchant
|
|
|
322
432
|
ssl_post(test ? TEST_URL : LIVE_URL, request.gsub("\n",''))
|
|
323
433
|
end
|
|
324
434
|
|
|
325
|
-
def
|
|
326
|
-
|
|
435
|
+
def handle_incorrect_currency_codes(currency)
|
|
436
|
+
case currency
|
|
437
|
+
when /UKL/i then 'GBP'
|
|
438
|
+
when /SID/i then 'SGD'
|
|
439
|
+
else currency
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def parse_transit_times(times)
|
|
444
|
+
results = []
|
|
445
|
+
times.each do |day_count|
|
|
446
|
+
days = TransitTimes.index(day_count.to_s.chomp)
|
|
447
|
+
results << days.to_i
|
|
448
|
+
end
|
|
449
|
+
results
|
|
327
450
|
end
|
|
328
451
|
end
|
|
329
452
|
end
|
|
@@ -82,6 +82,7 @@ module ActiveMerchant
|
|
|
82
82
|
xml.tag! 'Address1', destination.address1
|
|
83
83
|
xml.tag! 'Address2', destination.address2 unless destination.address2.blank?
|
|
84
84
|
xml.tag! 'Address3', destination.address3 unless destination.address3.blank?
|
|
85
|
+
xml.tag! 'Company', destination.company unless destination.company.blank?
|
|
85
86
|
xml.tag! 'City', destination.city
|
|
86
87
|
xml.tag! 'State', destination.state unless destination.state.blank?
|
|
87
88
|
xml.tag! 'Country', destination.country_code
|
|
@@ -173,17 +174,6 @@ module ActiveMerchant
|
|
|
173
174
|
element.attributes[attribute]
|
|
174
175
|
end
|
|
175
176
|
end
|
|
176
|
-
|
|
177
|
-
def timestamp_from_business_day(days)
|
|
178
|
-
return unless days
|
|
179
|
-
date = DateTime.now
|
|
180
|
-
days.times do
|
|
181
|
-
begin
|
|
182
|
-
date = date + 1
|
|
183
|
-
end until ![0,6].include?(date.wday)
|
|
184
|
-
end
|
|
185
|
-
date
|
|
186
|
-
end
|
|
187
177
|
end
|
|
188
178
|
end
|
|
189
179
|
end
|
|
@@ -26,13 +26,13 @@ module ActiveMerchant
|
|
|
26
26
|
:letter_center => "19",
|
|
27
27
|
:air_service_center => "20"
|
|
28
28
|
})
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
CUSTOMER_CLASSIFICATIONS = HashWithIndifferentAccess.new({
|
|
31
31
|
:wholesale => "01",
|
|
32
32
|
:occasional => "03",
|
|
33
33
|
:retail => "04"
|
|
34
34
|
})
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
# these are the defaults described in the UPS API docs,
|
|
37
37
|
# but they don't seem to apply them under all circumstances,
|
|
38
38
|
# so we need to take matters into our own hands
|
|
@@ -85,7 +85,15 @@ module ActiveMerchant
|
|
|
85
85
|
OTHER_NON_US_ORIGIN_SERVICES = {
|
|
86
86
|
"07" => "UPS Express"
|
|
87
87
|
}
|
|
88
|
-
|
|
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
|
+
|
|
89
97
|
# From http://en.wikipedia.org/w/index.php?title=European_Union&oldid=174718707 (Current as of November 30, 2007)
|
|
90
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"]
|
|
91
99
|
|
|
@@ -276,7 +284,7 @@ module ActiveMerchant
|
|
|
276
284
|
xml.elements.each('/*/RatedShipment') do |rated_shipment|
|
|
277
285
|
service_code = rated_shipment.get_text('Service/Code').to_s
|
|
278
286
|
days_to_delivery = rated_shipment.get_text('GuaranteedDaysToDelivery').to_s.to_i
|
|
279
|
-
|
|
287
|
+
days_to_delivery = nil if days_to_delivery == 0
|
|
280
288
|
|
|
281
289
|
rate_estimates << RateEstimate.new(origin, destination, @@name,
|
|
282
290
|
service_name_for(origin, service_code),
|
|
@@ -284,7 +292,7 @@ module ActiveMerchant
|
|
|
284
292
|
:currency => rated_shipment.get_text('TotalCharges/CurrencyCode').to_s,
|
|
285
293
|
:service_code => service_code,
|
|
286
294
|
:packages => packages,
|
|
287
|
-
:delivery_range => [
|
|
295
|
+
:delivery_range => [timestamp_from_business_day(days_to_delivery)])
|
|
288
296
|
end
|
|
289
297
|
end
|
|
290
298
|
RateResponse.new(success, message, Hash.from_xml(response).values.first, :rates => rate_estimates, :xml => response, :request => last_request)
|
|
@@ -296,17 +304,39 @@ module ActiveMerchant
|
|
|
296
304
|
message = response_message(xml)
|
|
297
305
|
|
|
298
306
|
if success
|
|
299
|
-
tracking_number, origin, destination = nil
|
|
307
|
+
tracking_number, origin, destination, status_code, status_description = nil
|
|
308
|
+
delivered, exception = false
|
|
309
|
+
exception_event = nil
|
|
300
310
|
shipment_events = []
|
|
301
|
-
|
|
311
|
+
status = {}
|
|
312
|
+
scheduled_delivery_date = nil
|
|
313
|
+
|
|
302
314
|
first_shipment = xml.elements['/*/Shipment']
|
|
303
315
|
first_package = first_shipment.elements['Package']
|
|
304
316
|
tracking_number = first_shipment.get_text('ShipmentIdentificationNumber | Package/TrackingNumber').to_s
|
|
305
317
|
|
|
318
|
+
# Build status hash
|
|
319
|
+
status_node = first_package.elements['Activity/Status/StatusType']
|
|
320
|
+
status_code = status_node.get_text('Code').to_s
|
|
321
|
+
status_description = status_node.get_text('Description').to_s
|
|
322
|
+
status = TRACKING_STATUS_CODES[status_code]
|
|
323
|
+
|
|
324
|
+
if status_description =~ /out.*delivery/i
|
|
325
|
+
status = :out_for_delivery
|
|
326
|
+
end
|
|
327
|
+
|
|
306
328
|
origin, destination = %w{Shipper ShipTo}.map do |location|
|
|
307
329
|
location_from_address_node(first_shipment.elements["#{location}/Address"])
|
|
308
330
|
end
|
|
309
|
-
|
|
331
|
+
|
|
332
|
+
# Get scheduled delivery date
|
|
333
|
+
unless status == :delivered
|
|
334
|
+
scheduled_delivery_date = parse_ups_datetime({
|
|
335
|
+
:date => first_shipment.get_text('ScheduledDeliveryDate'),
|
|
336
|
+
:time => nil
|
|
337
|
+
})
|
|
338
|
+
end
|
|
339
|
+
|
|
310
340
|
activities = first_package.get_elements('Activity')
|
|
311
341
|
unless activities.empty?
|
|
312
342
|
shipment_events = activities.map do |activity|
|
|
@@ -324,7 +354,10 @@ module ActiveMerchant
|
|
|
324
354
|
|
|
325
355
|
shipment_events = shipment_events.sort_by(&:time)
|
|
326
356
|
|
|
327
|
-
|
|
357
|
+
# UPS will sometimes archive a shipment, stripping all shipment activity except for the delivery
|
|
358
|
+
# event (see test/fixtures/xml/delivered_shipment_without_events_tracking_response.xml for an example).
|
|
359
|
+
# This adds an origin event to the shipment activity in such cases.
|
|
360
|
+
if origin && !(shipment_events.count == 1 && status == :delivered)
|
|
328
361
|
first_event = shipment_events[0]
|
|
329
362
|
same_country = origin.country_code(:alpha2) == first_event.location.country_code(:alpha2)
|
|
330
363
|
same_or_blank_city = first_event.location.city.blank? or first_event.location.city == origin.city
|
|
@@ -335,16 +368,29 @@ module ActiveMerchant
|
|
|
335
368
|
shipment_events.unshift(origin_event)
|
|
336
369
|
end
|
|
337
370
|
end
|
|
338
|
-
|
|
371
|
+
|
|
372
|
+
# Has the shipment been delivered?
|
|
373
|
+
if status == :delivered
|
|
374
|
+
if !destination
|
|
375
|
+
destination = shipment_events[-1].location
|
|
376
|
+
end
|
|
339
377
|
shipment_events[-1] = ShipmentEvent.new(shipment_events.last.name, shipment_events.last.time, destination)
|
|
340
378
|
end
|
|
341
379
|
end
|
|
342
380
|
|
|
343
381
|
end
|
|
344
382
|
TrackingResponse.new(success, message, Hash.from_xml(response).values.first,
|
|
383
|
+
:carrier => @@name,
|
|
345
384
|
:xml => response,
|
|
346
385
|
:request => last_request,
|
|
386
|
+
:status => status,
|
|
387
|
+
:status_code => status_code,
|
|
388
|
+
:status_description => status_description,
|
|
389
|
+
:scheduled_delivery_date => scheduled_delivery_date,
|
|
347
390
|
:shipment_events => shipment_events,
|
|
391
|
+
:delivered => delivered,
|
|
392
|
+
:exception => exception,
|
|
393
|
+
:exception_event => exception_event,
|
|
348
394
|
:origin => origin,
|
|
349
395
|
:destination => destination,
|
|
350
396
|
:tracking_number => tracking_number)
|
|
@@ -363,6 +409,18 @@ module ActiveMerchant
|
|
|
363
409
|
)
|
|
364
410
|
end
|
|
365
411
|
|
|
412
|
+
def parse_ups_datetime(options = {})
|
|
413
|
+
time, date = options[:time].to_s, options[:date].to_s
|
|
414
|
+
if time.nil?
|
|
415
|
+
hour, minute, second = 0
|
|
416
|
+
else
|
|
417
|
+
hour, minute, second = time.scan(/\d{2}/)
|
|
418
|
+
end
|
|
419
|
+
year, month, day = date[0..3], date[4..5], date[6..7]
|
|
420
|
+
|
|
421
|
+
Time.utc(year, month, day, hour, minute, second)
|
|
422
|
+
end
|
|
423
|
+
|
|
366
424
|
def response_success?(xml)
|
|
367
425
|
xml.get_text('/*/Response/ResponseStatusCode').to_s == '1'
|
|
368
426
|
end
|
|
@@ -131,6 +131,18 @@ module ActiveMerchant #:nodoc:
|
|
|
131
131
|
string << "\nFax: #{@fax}" unless @fax.blank?
|
|
132
132
|
string
|
|
133
133
|
end
|
|
134
|
+
|
|
135
|
+
# Returns the postal code as a properly formatted Zip+4 code, e.g. "77095-2233"
|
|
136
|
+
def zip_plus_4
|
|
137
|
+
if /(\d{5})(\d{4})/ =~ @postal_code
|
|
138
|
+
return "#{$1}-#{$2}"
|
|
139
|
+
elsif /\d{5}-\d{4}/ =~ @postal_code
|
|
140
|
+
return @postal_code
|
|
141
|
+
else
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
134
146
|
end
|
|
135
147
|
|
|
136
148
|
end
|
|
@@ -2,20 +2,45 @@ module ActiveMerchant #:nodoc:
|
|
|
2
2
|
module Shipping
|
|
3
3
|
|
|
4
4
|
class TrackingResponse < Response
|
|
5
|
+
attr_reader :carrier # symbol
|
|
6
|
+
attr_reader :carrier_name # string
|
|
7
|
+
attr_reader :status # symbol
|
|
8
|
+
attr_reader :status_code # string
|
|
9
|
+
attr_reader :status_description #string
|
|
10
|
+
attr_reader :scheduled_delivery_date # time
|
|
5
11
|
attr_reader :tracking_number # string
|
|
6
12
|
attr_reader :shipment_events # array of ShipmentEvents in chronological order
|
|
7
13
|
attr_reader :origin, :destination
|
|
8
14
|
|
|
9
15
|
def initialize(success, message, params = {}, options = {})
|
|
16
|
+
@carrier = options[:carrier].parameterize.to_sym
|
|
17
|
+
@carrier_name = options[:carrier]
|
|
18
|
+
@status = options[:status]
|
|
19
|
+
@status_code = options[:status_code]
|
|
20
|
+
@status_description = options[:status_description]
|
|
21
|
+
@scheduled_delivery_date = options[:scheduled_delivery_date]
|
|
10
22
|
@tracking_number = options[:tracking_number]
|
|
11
23
|
@shipment_events = Array(options[:shipment_events])
|
|
12
24
|
@origin, @destination = options[:origin], options[:destination]
|
|
13
25
|
super
|
|
14
26
|
end
|
|
15
|
-
|
|
27
|
+
|
|
16
28
|
def latest_event
|
|
17
29
|
@shipment_events.last
|
|
18
30
|
end
|
|
31
|
+
|
|
32
|
+
def is_delivered?
|
|
33
|
+
@status == :delivered
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def has_exception?
|
|
37
|
+
@status == :exception
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
alias_method(:exception_event, :latest_event)
|
|
41
|
+
alias_method(:delivered?, :is_delivered?)
|
|
42
|
+
alias_method(:exception?, :has_exception?)
|
|
43
|
+
|
|
19
44
|
end
|
|
20
45
|
|
|
21
46
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_shipping
|
|
3
|
-
version: !ruby/object:Gem::Version
|
|
4
|
-
hash: 39
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
5
4
|
prerelease:
|
|
6
|
-
|
|
7
|
-
- 0
|
|
8
|
-
- 9
|
|
9
|
-
- 14
|
|
10
|
-
version: 0.9.14
|
|
5
|
+
version: 0.9.15
|
|
11
6
|
platform: ruby
|
|
12
|
-
authors:
|
|
7
|
+
authors:
|
|
13
8
|
- James MacAulay
|
|
14
9
|
- Tobi Lutke
|
|
15
10
|
- Cody Fauser
|
|
@@ -17,124 +12,143 @@ authors:
|
|
|
17
12
|
autorequire:
|
|
18
13
|
bindir: bin
|
|
19
14
|
cert_chain: []
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
dependencies:
|
|
24
|
-
- !ruby/object:Gem::Dependency
|
|
25
|
-
name: activesupport
|
|
15
|
+
date: 2013-05-06 00:00:00.000000000 Z
|
|
16
|
+
dependencies:
|
|
17
|
+
- !ruby/object:Gem::Dependency
|
|
26
18
|
prerelease: false
|
|
27
|
-
|
|
28
|
-
none: false
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
hash: 9
|
|
33
|
-
segments:
|
|
34
|
-
- 2
|
|
35
|
-
- 3
|
|
36
|
-
- 5
|
|
37
|
-
version: 2.3.5
|
|
19
|
+
name: activesupport
|
|
38
20
|
type: :runtime
|
|
39
|
-
version_requirements:
|
|
40
|
-
|
|
41
|
-
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ! '>='
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 2.3.5
|
|
26
|
+
none: false
|
|
27
|
+
requirement: !ruby/object:Gem::Requirement
|
|
28
|
+
requirements:
|
|
29
|
+
- - ! '>='
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 2.3.5
|
|
32
|
+
none: false
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
42
34
|
prerelease: false
|
|
43
|
-
|
|
44
|
-
none: false
|
|
45
|
-
requirements:
|
|
46
|
-
- - ">="
|
|
47
|
-
- !ruby/object:Gem::Version
|
|
48
|
-
hash: 3
|
|
49
|
-
segments:
|
|
50
|
-
- 0
|
|
51
|
-
version: "0"
|
|
35
|
+
name: i18n
|
|
52
36
|
type: :runtime
|
|
53
|
-
version_requirements:
|
|
54
|
-
|
|
55
|
-
|
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ! '>='
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '0'
|
|
42
|
+
none: false
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ! '>='
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
none: false
|
|
49
|
+
- !ruby/object:Gem::Dependency
|
|
56
50
|
prerelease: false
|
|
57
|
-
|
|
58
|
-
none: false
|
|
59
|
-
requirements:
|
|
60
|
-
- - ">="
|
|
61
|
-
- !ruby/object:Gem::Version
|
|
62
|
-
hash: 21
|
|
63
|
-
segments:
|
|
64
|
-
- 1
|
|
65
|
-
- 0
|
|
66
|
-
- 1
|
|
67
|
-
version: 1.0.1
|
|
51
|
+
name: active_utils
|
|
68
52
|
type: :runtime
|
|
69
|
-
version_requirements:
|
|
70
|
-
|
|
71
|
-
|
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ! '>='
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: 1.0.1
|
|
58
|
+
none: false
|
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ! '>='
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: 1.0.1
|
|
64
|
+
none: false
|
|
65
|
+
- !ruby/object:Gem::Dependency
|
|
72
66
|
prerelease: false
|
|
73
|
-
|
|
74
|
-
none: false
|
|
75
|
-
requirements:
|
|
76
|
-
- - ">="
|
|
77
|
-
- !ruby/object:Gem::Version
|
|
78
|
-
hash: 3
|
|
79
|
-
segments:
|
|
80
|
-
- 0
|
|
81
|
-
version: "0"
|
|
67
|
+
name: builder
|
|
82
68
|
type: :runtime
|
|
83
|
-
version_requirements:
|
|
84
|
-
|
|
85
|
-
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ! '>='
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
none: false
|
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ! '>='
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '0'
|
|
80
|
+
none: false
|
|
81
|
+
- !ruby/object:Gem::Dependency
|
|
86
82
|
prerelease: false
|
|
87
|
-
|
|
88
|
-
none: false
|
|
89
|
-
requirements:
|
|
90
|
-
- - ">="
|
|
91
|
-
- !ruby/object:Gem::Version
|
|
92
|
-
hash: 1
|
|
93
|
-
segments:
|
|
94
|
-
- 1
|
|
95
|
-
- 5
|
|
96
|
-
- 1
|
|
97
|
-
version: 1.5.1
|
|
83
|
+
name: json
|
|
98
84
|
type: :runtime
|
|
99
|
-
version_requirements:
|
|
100
|
-
|
|
101
|
-
|
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ! '>='
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 1.5.1
|
|
90
|
+
none: false
|
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ! '>='
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 1.5.1
|
|
96
|
+
none: false
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
102
98
|
prerelease: false
|
|
103
|
-
|
|
104
|
-
none: false
|
|
105
|
-
requirements:
|
|
106
|
-
- - ">="
|
|
107
|
-
- !ruby/object:Gem::Version
|
|
108
|
-
hash: 3
|
|
109
|
-
segments:
|
|
110
|
-
- 0
|
|
111
|
-
version: "0"
|
|
99
|
+
name: rake
|
|
112
100
|
type: :development
|
|
113
|
-
version_requirements:
|
|
114
|
-
|
|
101
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ! '>='
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
none: false
|
|
107
|
+
requirement: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ! '>='
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '0'
|
|
112
|
+
none: false
|
|
113
|
+
- !ruby/object:Gem::Dependency
|
|
114
|
+
prerelease: false
|
|
115
115
|
name: mocha
|
|
116
|
+
type: :development
|
|
117
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - ! '>='
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '0'
|
|
122
|
+
none: false
|
|
123
|
+
requirement: !ruby/object:Gem::Requirement
|
|
124
|
+
requirements:
|
|
125
|
+
- - ! '>='
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: '0'
|
|
128
|
+
none: false
|
|
129
|
+
- !ruby/object:Gem::Dependency
|
|
116
130
|
prerelease: false
|
|
117
|
-
|
|
118
|
-
none: false
|
|
119
|
-
requirements:
|
|
120
|
-
- - ">="
|
|
121
|
-
- !ruby/object:Gem::Version
|
|
122
|
-
hash: 3
|
|
123
|
-
segments:
|
|
124
|
-
- 0
|
|
125
|
-
version: "0"
|
|
131
|
+
name: timecop
|
|
126
132
|
type: :development
|
|
127
|
-
version_requirements:
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ! '>='
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
138
|
+
none: false
|
|
139
|
+
requirement: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - ! '>='
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '0'
|
|
144
|
+
none: false
|
|
128
145
|
description: Get rates and tracking info from various shipping carriers.
|
|
129
|
-
email:
|
|
146
|
+
email:
|
|
130
147
|
- james@shopify.com
|
|
131
148
|
executables: []
|
|
132
|
-
|
|
133
149
|
extensions: []
|
|
134
|
-
|
|
135
150
|
extra_rdoc_files: []
|
|
136
|
-
|
|
137
|
-
files:
|
|
151
|
+
files:
|
|
138
152
|
- lib/active_shipping/shipping/base.rb
|
|
139
153
|
- lib/active_shipping/shipping/carrier.rb
|
|
140
154
|
- lib/active_shipping/shipping/carriers/bogus_carrier.rb
|
|
@@ -179,41 +193,31 @@ files:
|
|
|
179
193
|
- MIT-LICENSE
|
|
180
194
|
- README.markdown
|
|
181
195
|
- CHANGELOG
|
|
182
|
-
has_rdoc: true
|
|
183
196
|
homepage: http://github.com/shopify/active_shipping
|
|
184
197
|
licenses: []
|
|
185
|
-
|
|
186
198
|
post_install_message:
|
|
187
199
|
rdoc_options: []
|
|
188
|
-
|
|
189
|
-
require_paths:
|
|
200
|
+
require_paths:
|
|
190
201
|
- lib
|
|
191
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
hash: 3
|
|
197
|
-
segments:
|
|
202
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
203
|
+
requirements:
|
|
204
|
+
- - ! '>='
|
|
205
|
+
- !ruby/object:Gem::Version
|
|
206
|
+
segments:
|
|
198
207
|
- 0
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
hash: 3014508717412085176
|
|
209
|
+
version: '0'
|
|
201
210
|
none: false
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
segments:
|
|
207
|
-
- 1
|
|
208
|
-
- 3
|
|
209
|
-
- 6
|
|
211
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
212
|
+
requirements:
|
|
213
|
+
- - ! '>='
|
|
214
|
+
- !ruby/object:Gem::Version
|
|
210
215
|
version: 1.3.6
|
|
216
|
+
none: false
|
|
211
217
|
requirements: []
|
|
212
|
-
|
|
213
218
|
rubyforge_project: active_shipping
|
|
214
|
-
rubygems_version: 1.
|
|
219
|
+
rubygems_version: 1.8.23
|
|
215
220
|
signing_key:
|
|
216
221
|
specification_version: 3
|
|
217
222
|
summary: Shipping API extension for Active Merchant
|
|
218
223
|
test_files: []
|
|
219
|
-
|