trackerific 0.2.1 → 0.3.0

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