trackerific 0.2.1 → 0.3.0

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.
@@ -0,0 +1,103 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4
+ <head>
5
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
6
+ <title>
7
+ Top Level Namespace
8
+
9
+ &mdash; Documentation by YARD 0.7.1
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" media="screen" charset="utf-8" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" media="screen" charset="utf-8" />
16
+
17
+ <script type="text/javascript" charset="utf-8">
18
+ relpath = '';
19
+ if (relpath != '') relpath += '/';
20
+ </script>
21
+
22
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
23
+
24
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
25
+
26
+
27
+ </head>
28
+ <body>
29
+ <script type="text/javascript" charset="utf-8">
30
+ if (window.top.frames.main) document.body.className = 'frames';
31
+ </script>
32
+
33
+ <div id="header">
34
+ <div id="menu">
35
+
36
+ <a href="_index.html">Index</a> &raquo;
37
+
38
+
39
+ <span class="title">Top Level Namespace</span>
40
+
41
+
42
+ <div class="noframes"><span class="title">(</span><a href="." target="_top">no frames</a><span class="title">)</span></div>
43
+ </div>
44
+
45
+ <div id="search">
46
+
47
+ <a id="class_list_link" href="#">Class List</a>
48
+
49
+ <a id="method_list_link" href="#">Method List</a>
50
+
51
+ <a id="file_list_link" href="#">File List</a>
52
+
53
+ </div>
54
+ <div class="clear"></div>
55
+ </div>
56
+
57
+ <iframe id="search_frame"></iframe>
58
+
59
+ <div id="content"><h1>Top Level Namespace
60
+
61
+
62
+
63
+ </h1>
64
+
65
+ <dl class="box">
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+ </dl>
75
+ <div class="clear"></div>
76
+
77
+ <h2>Defined Under Namespace</h2>
78
+ <p class="children">
79
+
80
+
81
+ <strong class="modules">Modules:</strong> <span class='object_link'><a href="Trackerific.html" title="Trackerific (module)">Trackerific</a></span>
82
+
83
+
84
+
85
+
86
+ </p>
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+ </div>
95
+
96
+ <div id="footer">
97
+ Generated on Mon Jun 13 13:53:34 2011 by
98
+ <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
99
+ 0.7.1 (ruby-1.9.2).
100
+ </div>
101
+
102
+ </body>
103
+ </html>
data/lib/fedex.rb CHANGED
@@ -2,15 +2,23 @@ require 'httparty'
2
2
 
3
3
  module Trackerific
4
4
 
5
+ # Provides package tracking support for FedEx
5
6
  class FedEx < Base
6
7
  include ::HTTParty
7
8
  format :xml
8
9
  base_uri "https://gateway.fedex.com"
9
10
 
11
+ # @return [Array] required options for tracking a FedEx package are :account
12
+ # and :meter.
10
13
  def required_options
11
14
  [:account, :meter]
12
15
  end
13
16
 
17
+ # Tracks a FedEx package.
18
+ #
19
+ # A Trackerific::Error is raised when a package cannot be tracked.
20
+ #
21
+ # @return [Trackerific::Details] the tracking details
14
22
  def track_package(package_id)
15
23
  super
16
24
  http_response = self.class.post "/GatewayDC", :body => build_xml_request
@@ -22,23 +30,25 @@ module Trackerific
22
30
  details = track_reply["Package"]
23
31
  events = []
24
32
  details["Event"].each do |e|
25
- date = Time.parse("#{e["Date"]} #{e["Time"]}").strftime('%b %d %I:%M %P')
33
+ date = Time.parse("#{e["Date"]} #{e["Time"]}")
26
34
  desc = e["Description"]
27
35
  addr = e["Address"]
28
36
  # Adds event in this format:
29
37
  # MM DD HH:MM am/pm Description City State Zip
30
- events << "#{date} #{desc} #{addr["City"]} #{addr["StateOrProvinceCode"]} #{addr["PostalCode"]}"
38
+ events << Trackerific::Event.new(date, desc, "#{addr["StateOrProvinceCode"]} #{addr["PostalCode"]}")
31
39
  end
32
40
 
33
- return {
34
- :package_id => details["TrackingNumber"],
35
- :summary => details["StatusDescription"],
36
- :details => events
37
- }
41
+ Details.new(
42
+ details["TrackingNumber"],
43
+ details["StatusDescription"],
44
+ events
45
+ )
38
46
  end
39
47
 
40
48
  protected
41
49
 
50
+ # Builds the XML request to send to FedEx
51
+ # @return [String] a FDXTrack2Request XML
42
52
  def build_xml_request
43
53
  xml = ""
44
54
  xmlns_api = "http://www.fedex.com/fsmapi"
data/lib/trackerific.rb CHANGED
@@ -1,9 +1,13 @@
1
+ # Trackerific is a UPS, FedEx and USPS tracking provider.
1
2
  module Trackerific
2
3
  require 'rails'
4
+ require 'trackerific_details'
5
+ require 'trackerific_event'
3
6
 
4
- class Error < StandardError
5
- end
7
+ # Raised if something other than tracking information is returned.
8
+ class Error < StandardError; end
6
9
 
10
+ # Base class for Trackerific package tracking services.
7
11
  class Base
8
12
  def initialize(options = {})
9
13
  required = required_options
@@ -16,10 +20,14 @@ module Trackerific
16
20
  @options = options
17
21
  end
18
22
 
23
+ # Override this method if your subclass has required options.
24
+ # @return [Array] the required options
19
25
  def required_options
20
26
  []
21
27
  end
22
28
 
29
+ # Override this method in your subclass to implement tracking a package.
30
+ # @return [Hash] the tracking details
23
31
  def track_package(package_id)
24
32
  @package_id = package_id
25
33
  end
@@ -29,6 +37,12 @@ module Trackerific
29
37
  require 'fedex'
30
38
  require 'ups'
31
39
 
40
+ # Checks a string for a valid package identifier, and returns a Trackerific
41
+ # class that should be able to track the package.
42
+ #
43
+ # @param [String] the package identifier.
44
+ # @return [Trackerific::Base] the Trackerific class that can track the given
45
+ # package id, or nil if none found.
32
46
  def tracking_service(package_id)
33
47
  case package_id
34
48
  when /^.Z/, /^[HK].{10}$/ then Trackerific::UPS
@@ -0,0 +1,30 @@
1
+ module Trackerific
2
+ # Details returned when tracking a package. Stores the package identifier,
3
+ # a summary, and the events.
4
+ class Details
5
+ # Provides a new instance of Details
6
+ # @param [String] the package identifier
7
+ # @param [String] a summary of the tracking status
8
+ # @param [Array, Trackerific::Event] the tracking events
9
+ def initialize(package_id, summary, events)
10
+ @package_id = package_id
11
+ @summary = summary
12
+ @events = events
13
+ end
14
+
15
+ # @return [String] the package identifier
16
+ def package_id
17
+ @package_id
18
+ end
19
+
20
+ # @return [String] a summary of the tracking status
21
+ def summary
22
+ @summary
23
+ end
24
+
25
+ # @return [Array, Trackerific::Event] the tracking events
26
+ def events
27
+ @events
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,38 @@
1
+ module Trackerific
2
+ # Provides details for a tracking event
3
+ class Event
4
+ # Provides a new instance of Event
5
+ # @param [Time] the date / time of the event
6
+ # @param [String] the event's description
7
+ # @param [String] where the event took place
8
+ def initialize(date, description, location)
9
+ @date = date
10
+ @description = description
11
+ @location = location
12
+ end
13
+
14
+ # @return [Time] the date / time of the event
15
+ def date
16
+ @date
17
+ end
18
+
19
+ # @return [String] the event's description.
20
+ def description
21
+ @description
22
+ end
23
+
24
+ # @return [String] where the event took place (usually in City State Zip
25
+ # format)
26
+ def location
27
+ @location
28
+ end
29
+
30
+ # @return [String] converts the event into a string
31
+ def to_s
32
+ dte = self.date.strftime('%b %d %I:%M %P')
33
+ des = self.description
34
+ loc = self.location
35
+ "#{dte} #{des} #{loc}"
36
+ end
37
+ end
38
+ end
data/lib/ups.rb CHANGED
@@ -1,6 +1,9 @@
1
+ require 'date'
2
+
1
3
  module Trackerific
2
4
  require 'httparty'
3
5
 
6
+ # Provides package tracking support for UPS.
4
7
  class UPS < Base
5
8
  include ::HTTParty
6
9
  format :xml
@@ -9,14 +12,25 @@ module Trackerific
9
12
  when 'production' then 'https://www.ups.com/ups.app/xml'
10
13
  end : 'https://www.ups.com/ups.app/xml'
11
14
 
15
+ # The required options for tracking a UPS package are :key, :user_id, and
16
+ # :password.
17
+ #
18
+ # @return [Array] the required options for tracking a UPS package.
12
19
  def required_options
13
20
  [:key, :user_id, :password]
14
21
  end
15
22
 
23
+ # Tracks a UPS package.
24
+ # A Trackerific::Error is raised when a package cannot be tracked.
25
+ # @return [Trackerific::Details] the tracking details
16
26
  def track_package(package_id)
17
27
  super
28
+ # connect to UPS via HTTParty
18
29
  http_response = self.class.post('/Track', :body => build_xml_request)
30
+ # throw any HTTP errors
19
31
  http_response.error! unless http_response.code == 200
32
+ # Check the response for errors, return a Trackerific::Error, or parse
33
+ # the response from UPS and return a Trackerific::Details
20
34
  case http_response['TrackResponse']['Response']['ResponseStatusCode']
21
35
  when "0" then raise Trackerific::Error, parse_error_response(http_response)
22
36
  when "1" then return parse_success_response(http_response)
@@ -26,32 +40,44 @@ module Trackerific
26
40
 
27
41
  protected
28
42
 
43
+ # Parses the response from UPS
44
+ # @return [Trackerific::Details]
29
45
  def parse_success_response(http_response)
46
+ # get the activity from the UPS response
30
47
  activity = http_response['TrackResponse']['Shipment']['Package']['Activity']
48
+ # if there's only one activity in the list, we need to put it in an array
31
49
  activity = [activity] if activity.is_a? Hash
50
+ # UPS does not provide a summary, so we'll just use the last tracking status
51
+ summary = activity.first['Status']['StatusType']['Description'].titleize
32
52
  details = []
33
53
  activity.each do |a|
34
- status = a['Status']['StatusType']['Description']
35
- if status != "UPS INTERNAL ACTIVITY CODE"
36
- address = a['ActivityLocation']['Address'].map {|k,v| v}.join(" ")
37
- date = "#{a['Date'].to_date} #{a['Time'][0..1]}:#{a['Time'][2..3]}:#{a['Time'][4..5]}".to_datetime
38
- details << "#{date.strftime('%b %d %I:%M %P')} #{status}"
39
- end
54
+ # the time format from UPS is HHMMSS, which cannot be directly converted
55
+ # to a Ruby time.
56
+ hours = a['Time'][0..1]
57
+ minutes = a['Time'][2..3]
58
+ seconds = a['Time'][4..5]
59
+ date = Date.parse(a['Date'])
60
+ date = DateTime.parse("#{date} #{hours}:#{minutes}:#{seconds}")
61
+ desc = a['Status']['StatusType']['Description'].titleize
62
+ loc = a['ActivityLocation']['Address'].map {|k,v| v}.join(" ")
63
+ details << Trackerific::Event.new(date, desc, loc)
40
64
  end
41
- # UPS does not provide a summary, so just use the last tracking details
42
- summary = details.last
43
- details.delete(summary)
44
- return {
45
- :package_id => @package_id,
46
- :summary => summary,
47
- :details => details.reverse
48
- }
65
+
66
+ Trackerific::Details.new(
67
+ @package_id,
68
+ summary,
69
+ details
70
+ )
49
71
  end
50
72
 
73
+ # Parses a UPS tracking response, and returns any errors.
74
+ # @return [String] the UPS tracking error
51
75
  def parse_error_response(http_response)
52
76
  http_response['TrackResponse']['Response']['Error']['ErrorDescription']
53
77
  end
54
78
 
79
+ # Builds the XML request to send to UPS for tracking a package.
80
+ # @return [String] the XML request
55
81
  def build_xml_request
56
82
  xml = ""
57
83
  builder = ::Builder::XmlMarkup.new(:target => xml)
data/lib/usps.rb CHANGED
@@ -1,7 +1,10 @@
1
+ require 'date'
2
+
1
3
  module Trackerific
2
4
  require 'builder'
3
5
  require 'httparty'
4
6
 
7
+ # Provides package tracking support for USPS.
5
8
  class USPS < Base
6
9
  include HTTParty
7
10
  format :xml
@@ -15,10 +18,16 @@ module Trackerific
15
18
  @options = options
16
19
  end
17
20
 
21
+ # The required option for tracking a UPS package is :user_id
22
+ #
23
+ # @return [Array] the required options for tracking a UPS package.
18
24
  def required_options
19
25
  [:user_id]
20
26
  end
21
27
 
28
+ # Tracks a USPS package.
29
+ # A Trackerific::Error is raised when a package cannot be tracked.
30
+ # @return [Trackerific::Details] the tracking details
22
31
  def track_package(package_id)
23
32
  super
24
33
  response = self.class.get('/ShippingAPITest.dll', :query => {:API => 'TrackV2', :XML => build_xml_request}.to_query)
@@ -26,15 +35,29 @@ module Trackerific
26
35
  raise Trackerific::Error, response['Error']['Description'] unless response['Error'].nil?
27
36
  raise Trackerific::Error, "Tracking information not found in response from server." if response['TrackResponse'].nil?
28
37
  tracking_info = response['TrackResponse']['TrackInfo']
29
- {
30
- :package_id => tracking_info['ID'],
31
- :summary => tracking_info['TrackSummary'],
32
- :details => tracking_info['TrackDetail']
33
- }
38
+ details = []
39
+ tracking_info['TrackDetail'].each do |d|
40
+ # each tracking detail is a string in this format:
41
+ # MM DD HH:MM am/pm DESCRIPTION CITY STATE ZIP.
42
+ # unfortunately, it will not be possible to tell the difference between
43
+ # the location, and the summary. So, for USPS, the location will be in
44
+ # the summary
45
+ d = d.split(" ")
46
+ date = DateTime.parse(d[0..3].join(" "))
47
+ desc = d[4..d.length].join(" ")
48
+ details << Trackerific::Event.new(date, desc, "")
49
+ end
50
+ Trackerific::Details.new(
51
+ tracking_info['ID'],
52
+ tracking_info['TrackSummary'],
53
+ details
54
+ )
34
55
  end
35
56
 
36
57
  protected
37
58
 
59
+ # Parses the response from UPS
60
+ # @return [Trackerific::Details]
38
61
  def build_xml_request
39
62
  tracking_request = ""
40
63
  builder = ::Builder::XmlMarkup.new(:target => tracking_request)
@@ -19,17 +19,15 @@ describe "Trackerific::FedEx" do
19
19
  FEDEX_TRACK_URL,
20
20
  :body => load_fixture(:fedex_success_response)
21
21
  )
22
- @valid_response = {
23
- :package_id => "183689015000001",
24
- :summary => "Delivered",
25
- :details => [
26
- "Jul 01 10:43 am Delivered Gainesville GA 30506",
27
- "Jul 01 08:48 am On FedEx vehicle for delivery ATHENS GA 30601",
28
- "Jul 01 05:07 am At local FedEx facility ATHENS GA 30601"
29
- ]
30
- }
22
+ @tracking = @fedex.track_package(@package_id)
23
+ end
24
+ specify { @tracking.should be_a Trackerific::Details }
25
+ it "should have at least one event" do
26
+ @tracking.events.length.should >= 1
27
+ end
28
+ it "should have a summary" do
29
+ @tracking.summary.should_not be_empty
31
30
  end
32
- specify { @fedex.track_package(@package_id).should eq @valid_response }
33
31
  end
34
32
  context "with an error response from the server" do
35
33
  before(:all) do