trackerific 0.7.1 → 0.7.2

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