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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -0
  3. data/Gemfile.lock +43 -1
  4. data/README.rdoc +58 -60
  5. data/lib/trackerific.rb +17 -20
  6. data/lib/trackerific/builders/base/soap.rb +21 -0
  7. data/lib/trackerific/builders/base/xml.rb +38 -0
  8. data/lib/trackerific/builders/fedex.rb +46 -0
  9. data/lib/trackerific/builders/ups.rb +32 -0
  10. data/lib/trackerific/builders/usps.rb +19 -0
  11. data/lib/trackerific/environment.rb +11 -0
  12. data/lib/trackerific/error.rb +3 -1
  13. data/lib/trackerific/parsers/base.rb +35 -0
  14. data/lib/trackerific/parsers/fedex.rb +59 -0
  15. data/lib/trackerific/parsers/ups.rb +66 -0
  16. data/lib/trackerific/parsers/usps.rb +51 -0
  17. data/lib/trackerific/services/base.rb +23 -10
  18. data/lib/trackerific/services/concerns/soap.rb +45 -0
  19. data/lib/trackerific/services/concerns/xml.rb +44 -0
  20. data/lib/trackerific/services/fedex.rb +9 -79
  21. data/lib/trackerific/services/mock_service.rb +4 -12
  22. data/lib/trackerific/services/ups.rb +11 -95
  23. data/lib/trackerific/services/usps.rb +21 -181
  24. data/lib/trackerific/soap/wsdl.rb +17 -0
  25. data/lib/trackerific/version.rb +1 -1
  26. data/spec/fixtures/fedex/error.xml +1 -10
  27. data/spec/fixtures/fedex/success.xml +1 -74
  28. data/spec/fixtures/ups/malformed.xml +10 -0
  29. data/spec/fixtures/ups/request.xml +1 -0
  30. data/spec/fixtures/usps/malformed.xml +2 -0
  31. data/spec/fixtures/usps/request.xml +1 -0
  32. data/spec/lib/trackerific/builders/base/soap_spec.rb +29 -0
  33. data/spec/lib/trackerific/builders/base/xml_spec.rb +35 -0
  34. data/spec/lib/trackerific/builders/fedex_spec.rb +70 -0
  35. data/spec/lib/trackerific/builders/ups_spec.rb +11 -0
  36. data/spec/lib/trackerific/builders/usps_spec.rb +11 -0
  37. data/spec/lib/trackerific/environment_spec.rb +45 -0
  38. data/spec/lib/trackerific/parsers/base_spec.rb +62 -0
  39. data/spec/lib/trackerific/parsers/ups_spec.rb +71 -0
  40. data/spec/lib/trackerific/parsers/usps_spec.rb +77 -0
  41. data/spec/lib/trackerific/services/base_spec.rb +56 -0
  42. data/spec/lib/trackerific/services/concerns/soap_spec.rb +71 -0
  43. data/spec/lib/trackerific/services/concerns/xml_spec.rb +65 -0
  44. data/spec/lib/trackerific/services/fedex_spec.rb +43 -45
  45. data/spec/lib/trackerific/services/ups_spec.rb +23 -2
  46. data/spec/lib/trackerific/services/usps_spec.rb +44 -34
  47. data/spec/lib/trackerific/services_spec.rb +7 -0
  48. data/spec/lib/trackerific/soap/wsdl_spec.rb +29 -0
  49. data/spec/lib/trackerific/version_spec.rb +1 -1
  50. data/spec/lib/trackerific_spec.rb +2 -11
  51. data/spec/spec_helper.rb +17 -1
  52. data/spec/support/reload.rb +6 -0
  53. data/spec/support/trackerific.rb +7 -0
  54. data/trackerific.gemspec +2 -0
  55. data/vendor/wsdl/fedex/TrackService_v8.wsdl +2284 -0
  56. metadata +81 -7
  57. data/lib/trackerific/configuration.rb +0 -7
  58. data/spec/fixtures/usps/city_state_lookup.xml +0 -8
  59. 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
@@ -0,0 +1,11 @@
1
+ module Trackerific
2
+ class << self
3
+ def env
4
+ @env || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
5
+ end
6
+
7
+ def env=(value)
8
+ @env = value
9
+ end
10
+ end
11
+ end
@@ -1,4 +1,6 @@
1
1
  module Trackerific
2
2
  # Raised if something other than tracking information is returned.
3
- class Error < StandardError; end
3
+ class Error < StandardError
4
+ attr_accessor :request, :response
5
+ end
4
6
  end
@@ -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
- options = Trackerific.configuration[self.name] || {}
16
- new(options).track(id)
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
- # Gets the tracking information for the package from the server
37
- # @param [String] id The package identifier
38
- # @return [Trackerific::Details] The tracking details
39
- # @api semipublic
40
- def track(id)
41
- raise NotImplementedError,
42
- "You must implement this method in your service", caller
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
- include ::HTTParty
4
+ require 'trackerific/builders/fedex'
5
+ require 'trackerific/parsers/fedex'
6
+
7
+ include Concerns::SOAP
8
8
 
9
- format :xml
10
- base_uri "https://gateway.fedex.com"
9
+ register :fedex
11
10
 
12
- self.register :fedex
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