trackerific 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +6 -0
- data/Gemfile.lock +43 -1
- data/README.rdoc +58 -60
- data/lib/trackerific.rb +17 -20
- data/lib/trackerific/builders/base/soap.rb +21 -0
- data/lib/trackerific/builders/base/xml.rb +38 -0
- data/lib/trackerific/builders/fedex.rb +46 -0
- data/lib/trackerific/builders/ups.rb +32 -0
- data/lib/trackerific/builders/usps.rb +19 -0
- data/lib/trackerific/environment.rb +11 -0
- data/lib/trackerific/error.rb +3 -1
- data/lib/trackerific/parsers/base.rb +35 -0
- data/lib/trackerific/parsers/fedex.rb +59 -0
- data/lib/trackerific/parsers/ups.rb +66 -0
- data/lib/trackerific/parsers/usps.rb +51 -0
- data/lib/trackerific/services/base.rb +23 -10
- data/lib/trackerific/services/concerns/soap.rb +45 -0
- data/lib/trackerific/services/concerns/xml.rb +44 -0
- data/lib/trackerific/services/fedex.rb +9 -79
- data/lib/trackerific/services/mock_service.rb +4 -12
- data/lib/trackerific/services/ups.rb +11 -95
- data/lib/trackerific/services/usps.rb +21 -181
- data/lib/trackerific/soap/wsdl.rb +17 -0
- data/lib/trackerific/version.rb +1 -1
- data/spec/fixtures/fedex/error.xml +1 -10
- data/spec/fixtures/fedex/success.xml +1 -74
- data/spec/fixtures/ups/malformed.xml +10 -0
- data/spec/fixtures/ups/request.xml +1 -0
- data/spec/fixtures/usps/malformed.xml +2 -0
- data/spec/fixtures/usps/request.xml +1 -0
- data/spec/lib/trackerific/builders/base/soap_spec.rb +29 -0
- data/spec/lib/trackerific/builders/base/xml_spec.rb +35 -0
- data/spec/lib/trackerific/builders/fedex_spec.rb +70 -0
- data/spec/lib/trackerific/builders/ups_spec.rb +11 -0
- data/spec/lib/trackerific/builders/usps_spec.rb +11 -0
- data/spec/lib/trackerific/environment_spec.rb +45 -0
- data/spec/lib/trackerific/parsers/base_spec.rb +62 -0
- data/spec/lib/trackerific/parsers/ups_spec.rb +71 -0
- data/spec/lib/trackerific/parsers/usps_spec.rb +77 -0
- data/spec/lib/trackerific/services/base_spec.rb +56 -0
- data/spec/lib/trackerific/services/concerns/soap_spec.rb +71 -0
- data/spec/lib/trackerific/services/concerns/xml_spec.rb +65 -0
- data/spec/lib/trackerific/services/fedex_spec.rb +43 -45
- data/spec/lib/trackerific/services/ups_spec.rb +23 -2
- data/spec/lib/trackerific/services/usps_spec.rb +44 -34
- data/spec/lib/trackerific/services_spec.rb +7 -0
- data/spec/lib/trackerific/soap/wsdl_spec.rb +29 -0
- data/spec/lib/trackerific/version_spec.rb +1 -1
- data/spec/lib/trackerific_spec.rb +2 -11
- data/spec/spec_helper.rb +17 -1
- data/spec/support/reload.rb +6 -0
- data/spec/support/trackerific.rb +7 -0
- data/trackerific.gemspec +2 -0
- data/vendor/wsdl/fedex/TrackService_v8.wsdl +2284 -0
- metadata +81 -7
- data/lib/trackerific/configuration.rb +0 -7
- data/spec/fixtures/usps/city_state_lookup.xml +0 -8
- data/spec/lib/trackerific/configuration_spec.rb +0 -13
@@ -0,0 +1,19 @@
|
|
1
|
+
module Trackerific
|
2
|
+
module Builders
|
3
|
+
class USPS < Base::XML.new(:user_id, :package_id)
|
4
|
+
protected
|
5
|
+
|
6
|
+
def build
|
7
|
+
add_track_request
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def add_track_request
|
13
|
+
builder.TrackRequest(:USERID => user_id) do |t|
|
14
|
+
t.TrackID(:ID => package_id)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/trackerific/error.rb
CHANGED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Trackerific
|
2
|
+
module Parsers
|
3
|
+
class Base
|
4
|
+
def initialize(package_id, response)
|
5
|
+
@package_id = package_id
|
6
|
+
@response = response
|
7
|
+
end
|
8
|
+
|
9
|
+
def parse
|
10
|
+
@result ||= if response_error
|
11
|
+
response_error
|
12
|
+
else
|
13
|
+
Trackerific::Details.new(@package_id, summary, events)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def response_error
|
20
|
+
raise NotImplementedError,
|
21
|
+
"Override this method in your parser", caller
|
22
|
+
end
|
23
|
+
|
24
|
+
def summary
|
25
|
+
raise NotImplementedError,
|
26
|
+
"Override this method in your parser", caller
|
27
|
+
end
|
28
|
+
|
29
|
+
def events
|
30
|
+
raise NotImplementedError,
|
31
|
+
"Override this method in your parser", caller
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Trackerific
|
2
|
+
module Parsers
|
3
|
+
class FedEx < Parsers::Base
|
4
|
+
protected
|
5
|
+
|
6
|
+
def response_error
|
7
|
+
@response_error ||= if highest_severity == 'ERROR'
|
8
|
+
Trackerific::Error.new(notifications[:message])
|
9
|
+
else
|
10
|
+
false
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def summary
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def events
|
19
|
+
track_details.map do |detail|
|
20
|
+
Trackerific::Event.new(parse_date(detail), nil, location(detail))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def location(detail)
|
27
|
+
a = detail[:destination_address]
|
28
|
+
"#{a[:city]}, #{a[:state_or_province_code]} #{a[:country_code]}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse_date(detail)
|
32
|
+
detail[:ship_timestamp]
|
33
|
+
end
|
34
|
+
|
35
|
+
def track_reply
|
36
|
+
@response.hash[:envelope][:body][:track_reply]
|
37
|
+
end
|
38
|
+
|
39
|
+
def track_details
|
40
|
+
@track_details ||= begin
|
41
|
+
details = track_reply[:completed_track_details][:track_details]
|
42
|
+
details.select do |d|
|
43
|
+
d[:ship_timestamp].present? && d[:destination_address].present? &&
|
44
|
+
d[:destination_address][:city].present? &&
|
45
|
+
d[:destination_address][:state_or_province_code].present?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def highest_severity
|
51
|
+
track_reply[:highest_severity]
|
52
|
+
end
|
53
|
+
|
54
|
+
def notifications
|
55
|
+
track_reply[:notifications]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Trackerific
|
2
|
+
module Parsers
|
3
|
+
class UPS < Parsers::Base
|
4
|
+
protected
|
5
|
+
|
6
|
+
def response_error
|
7
|
+
@response_error ||= if @response.code != 200
|
8
|
+
Trackerific::Error.new("HTTP returned status #{@response.code}")
|
9
|
+
elsif response_status_code == :error
|
10
|
+
Trackerific::Error.new(error_response)
|
11
|
+
elsif response_status_code == :success
|
12
|
+
false
|
13
|
+
else
|
14
|
+
Trackerific::Error.new("Unknown status code from server.")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def summary
|
19
|
+
description(activity.first)
|
20
|
+
end
|
21
|
+
|
22
|
+
def events
|
23
|
+
activity.map do |a|
|
24
|
+
date = parse_ups_date_time(a['Date'], a['Time'])
|
25
|
+
Trackerific::Event.new(date, description(a), location(a))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def track_response
|
32
|
+
@response['TrackResponse']
|
33
|
+
end
|
34
|
+
|
35
|
+
def response_status_code
|
36
|
+
{ "0" => :error,
|
37
|
+
"1" => :success
|
38
|
+
}[track_response['Response']['ResponseStatusCode']]
|
39
|
+
end
|
40
|
+
|
41
|
+
def error_response
|
42
|
+
track_response['Response']['Error']['ErrorDescription']
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_ups_date_time(date, time)
|
46
|
+
hours, minutes, seconds = time.scan(/.{2}/)
|
47
|
+
DateTime.parse("#{Date.parse(date)} #{hours}:#{minutes}:#{seconds}")
|
48
|
+
end
|
49
|
+
|
50
|
+
def description(a)
|
51
|
+
a['Status']['StatusType']['Description']
|
52
|
+
end
|
53
|
+
|
54
|
+
def location(a)
|
55
|
+
a['ActivityLocation']['Address'].map {|k,v| v}.join(" ")
|
56
|
+
end
|
57
|
+
|
58
|
+
def activity
|
59
|
+
@activity ||= begin
|
60
|
+
a = track_response['Shipment']['Package']['Activity']
|
61
|
+
a.is_a?(Array) ? a : [a]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Trackerific
|
2
|
+
module Parsers
|
3
|
+
class USPS < Parsers::Base
|
4
|
+
protected
|
5
|
+
|
6
|
+
def response_error
|
7
|
+
@response_error ||= if @response.code != 200
|
8
|
+
Trackerific::Error.new("HTTP returned status #{@response.code}")
|
9
|
+
elsif @response['Error']
|
10
|
+
Trackerific::Error.new(@response['Error']['Description'])
|
11
|
+
elsif @response['TrackResponse'].nil? && @response['CityStateLookupResponse'].nil?
|
12
|
+
Trackerific::Error.new("Invalid response from server.")
|
13
|
+
else
|
14
|
+
false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def summary
|
19
|
+
tracking_info['TrackSummary']
|
20
|
+
end
|
21
|
+
|
22
|
+
def events
|
23
|
+
tracking_info.fetch('TrackDetail', []).map do |e|
|
24
|
+
Trackerific::Event.new(date(e), description(e), location(e))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def tracking_info
|
31
|
+
@response['TrackResponse']['TrackInfo']
|
32
|
+
end
|
33
|
+
|
34
|
+
def date(event)
|
35
|
+
d = event.split(" ")
|
36
|
+
DateTime.parse(d[0..3].join(" "))
|
37
|
+
end
|
38
|
+
|
39
|
+
def description(event)
|
40
|
+
d = event.split(" ")
|
41
|
+
d[4..d.length-4].join(" ")
|
42
|
+
end
|
43
|
+
|
44
|
+
def location(event)
|
45
|
+
d = event.gsub(".", "").split(" ")
|
46
|
+
city, state, zip = d.last(3)
|
47
|
+
"#{city}, #{state} #{zip}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module Trackerific
|
2
2
|
module Services
|
3
3
|
class Base
|
4
|
+
@name = nil
|
5
|
+
|
4
6
|
class << self
|
5
7
|
attr_accessor :name
|
6
8
|
|
@@ -11,15 +13,26 @@ module Trackerific
|
|
11
13
|
Trackerific::Services[self.name] = self
|
12
14
|
end
|
13
15
|
|
16
|
+
# Creates a new instance and calls #track with the given id
|
17
|
+
# @param id The package identifier
|
18
|
+
# @return Either a Trackerific::Details or Trackerific::Error
|
14
19
|
def track(id)
|
15
|
-
|
16
|
-
|
20
|
+
new.track(id)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Reads the credentials from Trackerific.config
|
24
|
+
# @return [Hash] The service's credentials
|
25
|
+
def credentials
|
26
|
+
Trackerific.config[name]
|
17
27
|
end
|
18
28
|
|
19
29
|
# Checks if the given package ID can be tracked by this service
|
20
30
|
# @param [String] id The package ID
|
21
31
|
# @return [Boolean] true when this service can track the given ID
|
32
|
+
# @note This will always be false if no credentials were found for the
|
33
|
+
# service in Trackerific.config
|
22
34
|
def can_track?(id)
|
35
|
+
return false if credentials.nil?
|
23
36
|
package_id_matchers.each {|m| return true if id =~ m }
|
24
37
|
false
|
25
38
|
end
|
@@ -31,15 +44,15 @@ module Trackerific
|
|
31
44
|
"You must implement this method in your service", caller
|
32
45
|
end
|
33
46
|
end
|
34
|
-
end
|
35
47
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
48
|
+
def initialize(credentials=self.class.credentials)
|
49
|
+
@credentials = credentials
|
50
|
+
|
51
|
+
if credentials.nil?
|
52
|
+
raise Trackerific::Error,
|
53
|
+
"Missing credentials for #{self.class.name}", caller
|
54
|
+
end
|
55
|
+
end
|
43
56
|
end
|
44
57
|
end
|
45
58
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Trackerific
|
2
|
+
module Services
|
3
|
+
module Concerns
|
4
|
+
module SOAP
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
@soap_track_operation = :track
|
9
|
+
@soap_builder = nil
|
10
|
+
@soap_parser = nil
|
11
|
+
@soap_wsdl = ""
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
attr_accessor :soap_track_operation
|
16
|
+
attr_accessor :soap_builder
|
17
|
+
attr_accessor :soap_parser
|
18
|
+
attr_accessor :soap_wsdl
|
19
|
+
end
|
20
|
+
|
21
|
+
def track(id)
|
22
|
+
operation = self.class.soap_track_operation
|
23
|
+
request = client.call(operation, message: builder(id).hash)
|
24
|
+
response = self.class.soap_parser.new(id, request).parse
|
25
|
+
response.is_a?(Trackerific::Error) ? raise(response) : response
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def builder(id)
|
31
|
+
members = self.class.soap_builder.members - [:package_id]
|
32
|
+
credentials = @credentials.values_at(*members)
|
33
|
+
credentials << id
|
34
|
+
self.class.soap_builder.new(*credentials)
|
35
|
+
end
|
36
|
+
|
37
|
+
def client
|
38
|
+
@client ||= Savon.client(
|
39
|
+
convert_request_keys_to: :camelcase,
|
40
|
+
wsdl: Trackerific::SOAP::WSDL.path(self.class.soap_wsdl))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Trackerific
|
2
|
+
module Services
|
3
|
+
module Concerns
|
4
|
+
module XML
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
@xml_endpoint = ""
|
9
|
+
@xml_parser = nil
|
10
|
+
@xml_builder = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
attr_accessor :xml_endpoint
|
15
|
+
attr_accessor :xml_parser
|
16
|
+
attr_accessor :xml_builder
|
17
|
+
end
|
18
|
+
|
19
|
+
# Gets the tracking information for the package from the server
|
20
|
+
# @param [String] id The package identifier
|
21
|
+
# @return [Trackerific::Details] The tracking details
|
22
|
+
# @api semipublic
|
23
|
+
def track(id)
|
24
|
+
response = self.class.xml_parser.new(id, http_response(id)).parse
|
25
|
+
raise(response) if response.is_a?(Trackerific::Error)
|
26
|
+
return response
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def http_response(id)
|
32
|
+
self.class.post(self.class.xml_endpoint, body: builder(id).xml)
|
33
|
+
end
|
34
|
+
|
35
|
+
def builder(id)
|
36
|
+
members = self.class.xml_builder.members - [:package_id]
|
37
|
+
credentials = @credentials.values_at(*members)
|
38
|
+
credentials << id
|
39
|
+
self.class.xml_builder.new(*credentials)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -1,91 +1,21 @@
|
|
1
|
-
require 'httparty'
|
2
|
-
require 'builder'
|
3
|
-
|
4
1
|
module Trackerific
|
5
2
|
module Services
|
6
3
|
class FedEx < Base
|
7
|
-
|
4
|
+
require 'trackerific/builders/fedex'
|
5
|
+
require 'trackerific/parsers/fedex'
|
6
|
+
|
7
|
+
include Concerns::SOAP
|
8
8
|
|
9
|
-
|
10
|
-
base_uri "https://gateway.fedex.com"
|
9
|
+
register :fedex
|
11
10
|
|
12
|
-
self.
|
11
|
+
self.soap_track_operation = :track
|
12
|
+
self.soap_builder = Builders::FedEx
|
13
|
+
self.soap_parser = Parsers::FedEx
|
14
|
+
self.soap_wsdl = 'fedex/TrackService_v8'
|
13
15
|
|
14
|
-
# An Array of Regexp that matches valid FedEx package IDs
|
15
|
-
# @return [Array, Regexp] the regular expression
|
16
|
-
# @api private
|
17
16
|
def self.package_id_matchers
|
18
17
|
[ /^[0-9]{15}$/ ]
|
19
18
|
end
|
20
|
-
|
21
|
-
def initialize(options={})
|
22
|
-
@options = options
|
23
|
-
end
|
24
|
-
|
25
|
-
# Tracks a FedEx package
|
26
|
-
# @param [String] package_id the package identifier
|
27
|
-
# @return [Trackerific::Details] the tracking details
|
28
|
-
# @raise [Trackerific::Error] raised when the server returns an error (invalid credentials, tracking package, etc.)
|
29
|
-
# @example Track a package
|
30
|
-
# fedex = Trackerific::FedEx.new :account => 'account', :meter => 'meter'
|
31
|
-
# details = fedex.track_package("183689015000001")
|
32
|
-
# @api public
|
33
|
-
def track(package_id)
|
34
|
-
# request tracking information from FedEx via HTTParty
|
35
|
-
http_response = self.class.post "/GatewayDC", body: build_xml_request
|
36
|
-
# raise any HTTP errors
|
37
|
-
http_response.error! unless http_response.code == 200
|
38
|
-
# get the tracking information from the reply
|
39
|
-
track_reply = http_response["FDXTrack2Reply"]
|
40
|
-
# raise a Trackerific::Error if there is an error in the reply
|
41
|
-
raise Trackerific::Error, track_reply["Error"]["Message"] unless track_reply["Error"].nil?
|
42
|
-
# get the details from the reply
|
43
|
-
details = track_reply["Package"]
|
44
|
-
# convert them into Trackerific::Events
|
45
|
-
events = []
|
46
|
-
details["Event"].each do |e|
|
47
|
-
date = Time.parse("#{e["Date"]} #{e["Time"]}")
|
48
|
-
desc = e["Description"]
|
49
|
-
addr = e["Address"]
|
50
|
-
loc = "#{addr["StateOrProvinceCode"]} #{addr["PostalCode"]}"
|
51
|
-
events << Trackerific::Event.new(date, desc, loc)
|
52
|
-
end
|
53
|
-
# Return a Trackerific::Details containing all the events
|
54
|
-
Trackerific::Details.new(
|
55
|
-
details["TrackingNumber"], details["StatusDescription"], events
|
56
|
-
)
|
57
|
-
end
|
58
|
-
|
59
|
-
private
|
60
|
-
|
61
|
-
# Builds the XML request to send to FedEx
|
62
|
-
# @return [String] a FDXTrack2Request XML
|
63
|
-
# @api private
|
64
|
-
def build_xml_request
|
65
|
-
xml = ""
|
66
|
-
# the API namespace
|
67
|
-
xmlns_api = "http://www.fedex.com/fsmapi"
|
68
|
-
# the XSI namespace
|
69
|
-
xmlns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
70
|
-
# the XSD namespace
|
71
|
-
xsi_noNSL = "FDXTrack2Request.xsd"
|
72
|
-
# create a new Builder to generate the XML
|
73
|
-
builder = ::Builder::XmlMarkup.new(:target => xml)
|
74
|
-
# add the XML header
|
75
|
-
builder.instruct! :xml, :version => "1.0", :encoding => "UTF-8"
|
76
|
-
# Build, and return the request
|
77
|
-
builder.FDXTrack2Request "xmlns:api"=>xmlns_api, "xmlns:xsi"=>xmlns_xsi, "xsi:noNamespaceSchemaLocation" => xsi_noNSL do |r|
|
78
|
-
r.RequestHeader do |rh|
|
79
|
-
rh.AccountNumber @options[:account]
|
80
|
-
rh.MeterNumber @options[:meter]
|
81
|
-
end
|
82
|
-
r.PackageIdentifier do |pi|
|
83
|
-
pi.Value @package_id
|
84
|
-
end
|
85
|
-
r.DetailScans true
|
86
|
-
end
|
87
|
-
xml
|
88
|
-
end
|
89
19
|
end
|
90
20
|
end
|
91
21
|
end
|