trackerific 0.5.5 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/README.rdoc +62 -44
  2. data/Rakefile +1 -1
  3. data/VERSION +1 -1
  4. data/changelog +9 -0
  5. data/lib/trackerific/configuration.rb +2 -2
  6. data/lib/trackerific/details.rb +36 -10
  7. data/lib/trackerific/event.rb +9 -7
  8. data/lib/trackerific/service.rb +22 -6
  9. data/lib/trackerific/services/fedex.rb +11 -7
  10. data/lib/trackerific/services/mock_service.rb +23 -14
  11. data/lib/trackerific/services/ups.rb +12 -8
  12. data/lib/trackerific/services/usps.rb +114 -21
  13. data/spec/fixtures/usps_city_state_lookup_response.xml +8 -0
  14. data/spec/lib/helpers/options_helper_spec.rb +3 -3
  15. data/spec/lib/trackerific/configuration_spec.rb +35 -0
  16. data/spec/lib/trackerific/details_spec.rb +71 -11
  17. data/spec/lib/trackerific/event_spec.rb +34 -19
  18. data/spec/lib/trackerific/service_spec.rb +3 -3
  19. data/spec/lib/trackerific/services/fedex_spec.rb +16 -3
  20. data/spec/lib/trackerific/services/mock_service_spec.rb +19 -4
  21. data/spec/lib/trackerific/services/ups_spec.rb +17 -2
  22. data/spec/lib/trackerific/services/usps_spec.rb +60 -11
  23. data/spec/lib/trackerific_spec.rb +7 -5
  24. data/trackerific.gemspec +5 -27
  25. metadata +8 -30
  26. data/doc/OptionsHelper.html +0 -287
  27. data/doc/Trackerific/Configuration.html +0 -354
  28. data/doc/Trackerific/Details.html +0 -565
  29. data/doc/Trackerific/Error.html +0 -127
  30. data/doc/Trackerific/Event.html +0 -639
  31. data/doc/Trackerific/FedEx.html +0 -558
  32. data/doc/Trackerific/Service.html +0 -579
  33. data/doc/Trackerific/UPS.html +0 -532
  34. data/doc/Trackerific/USPS.html +0 -568
  35. data/doc/Trackerific.html +0 -833
  36. data/doc/_index.html +0 -226
  37. data/doc/class_list.html +0 -47
  38. data/doc/css/common.css +0 -1
  39. data/doc/css/full_list.css +0 -53
  40. data/doc/css/style.css +0 -320
  41. data/doc/file.README.html +0 -288
  42. data/doc/file_list.html +0 -49
  43. data/doc/frames.html +0 -13
  44. data/doc/index.html +0 -288
  45. data/doc/js/app.js +0 -205
  46. data/doc/js/full_list.js +0 -150
  47. data/doc/js/jquery.js +0 -16
  48. data/doc/method_list.html +0 -294
  49. data/doc/top-level-namespace.html +0 -103
data/README.rdoc CHANGED
@@ -18,10 +18,20 @@ configure your credentials for each service.
18
18
  # config/initializers/trackerific.rb
19
19
  require 'trackerific'
20
20
  Trackerific.configure do |config|
21
- config.fedex :account => 'account', :meter => '123456789'
22
- config.ups :key => 'key', :user_id => 'userid', :password => 'secret'
23
- config.usps :user_id => 'userid'
21
+ config.fedex :account => 'account',
22
+ :meter => '123456789'
23
+
24
+ config.ups :key => 'key',
25
+ :user_id => 'userid',
26
+ :password => 'secret'
27
+
28
+ config.usps :user_id => 'userid',
29
+ :use_city_state_lookup => true
24
30
  end
31
+
32
+ For USPS packages, the option :use_city_state_lookup defaults to false, and will
33
+ only work if you have access to USPS's CityStateLookup API. If you can enable
34
+ it, this feature will provide the location for USPS package events.
25
35
 
26
36
  === Tracking with Automatic Service Discovery
27
37
 
@@ -85,6 +95,14 @@ Note that events.last will return the first event the tracking provider
85
95
  supplied. This is because the events are listed in LIFO order, so the most
86
96
  recent events will always be at the top of the list.
87
97
 
98
+ === City / State Lookup Via USPS
99
+
100
+ If you have access to the USPS CityStateLookup API, you can use Trackerific to
101
+ look up the city and state of a zipcode.
102
+
103
+ usps = Trackerific::USPS.new :user_id => 'userid'
104
+ usps.city_state_lookup "90210" # => { :city => 'BEVERLY HILLS', :state => 'CA', :zip => '90210' }
105
+
88
106
  === Exception handling
89
107
 
90
108
  Exception handling is esssential for tracking packages. If, for example,
@@ -98,60 +116,60 @@ example on how to handle Trackerific::Errors:
98
116
  puts e.message
99
117
  end
100
118
 
101
- == Extending
119
+ or for a Rails application:
120
+
121
+ # in app/controllers/application_controller.rb
122
+ rescue_from Trackerific::Error do |exception|
123
+ redirect_to root_url, :alert => exception.message
124
+ end
125
+
126
+ == Writing a Custom Service
102
127
 
103
- Here is a basic outline of a custom Trackerific service.
128
+ Here is a spec for writing a custom trackerific service:
104
129
 
105
- lib/trackerific/services/my_tracking_service.rb:
106
- module Trackerific
107
- class MyTrackingService < Trackerific::Base
108
- def self.required_options
109
- # any options your service requires. these are usually user credentials
110
- [ :some, :options ]
130
+ describe Trackerific::CustomService do
131
+ specify("it should descend from Trackerific::Service") {
132
+ Trackerific::CustomService.superclass.should be Trackerific::Service
133
+ }
134
+ describe :track_package do
135
+ before do
136
+ @valid_package_id = 'valid package id'
137
+ @invalid_package_id = 'invalid package id'
138
+ @service = Trackerific::CustomService.new :required => 'option'
111
139
  end
112
- def self.package_id_matchers
113
- # write some custom regex matchers for your tracking package IDs
114
- [ /^[0-9]{15}$/ ] # fedex package matcher
140
+ context "with a successful response from the server" do
141
+ before(:each) do
142
+ @tracking = @service.track_package(@valid_package_id)
143
+ end
144
+ subject { @tracking }
145
+ it("should return a Trackerific::Details") { should be_a Trackerific::Details }
146
+ describe :summary do
147
+ subject { @tracking.summary }
148
+ it { should_not be_empty }
149
+ end
115
150
  end
116
- def track_package(package_id)
117
- # implement your tracking code here
118
- Trackerific::Details.new(
119
- "summary",
120
- [
121
- Trackerific::Event.new(Time.now, "description", "location"),
122
- Trackerific::Event.new(Time.now, "description", "location")
123
- ]
124
- )
151
+ context "with an error response from the server" do
152
+ specify { lambda { @service.track_package(@invalid_package_id) }.should raise_error(Trackerific::Error) }
125
153
  end
126
154
  end
127
- end
128
-
129
- spec/lib/trackerific/services/my_tracking_service_spec.rb:
130
- describe "Trackerific::MyTrackingService" do
131
155
  describe :required_options do
132
- subject { Trackerific::MyTrackingService.required_options }
133
- it { should include(:some) }
134
- it { should include(:options) }
156
+ subject { Trackerific::CustomService.required_options }
157
+ it { should include(:required) }
135
158
  end
136
- describe :package_id_matchers do
137
- it "should be an Array of Regexp" do
138
- Trackerific::MyTrackingService.package_id_matchers.should each { |m| m.should be_a Regexp }
159
+ describe :valid_options do
160
+ it "should include required_options" do
161
+ valid = Trackerific::CustomService.valid_options
162
+ Trackerific::CustomService.required_options.each do |opt|
163
+ valid.should include opt
164
+ end
139
165
  end
140
166
  end
141
- describe :track_package do
142
- pending "your track_package specs"
167
+ describe :package_id_matchers do
168
+ subject { Trackerific::CustomService.package_id_matchers }
169
+ it("should be an Array of Regexp") { should each { |m| m.should be_a Regexp } }
143
170
  end
144
171
  end
145
172
 
146
- Please make sure to include comments, documentation, and specs for your service.
147
- Trackerific uses {RSpec}[https://github.com/dchelimsky/rspec] for tests,
148
- {simplecov}[https://github.com/colszowka/simplecov] for code coverage,
149
- and {Yardoc}[http://yardoc.org/] for documentation. You can also take advantage
150
- of {yardstick}[https://github.com/dkubb/yardstick] to help verify the coverage
151
- of the comments of your code. You can use the rake task:
152
- rake yardstick_measure
153
- which will generate a measurement/report.txt file.
154
-
155
173
  === Testing with Trackerific
156
174
 
157
175
  Trackerific provides a mocked service you can use in your unit tests of your
data/Rakefile CHANGED
@@ -16,7 +16,7 @@ Jeweler::Tasks.new do |gem|
16
16
  gem.homepage = "http://github.com/travishaynes/trackerific"
17
17
  gem.license = "MIT"
18
18
  gem.summary = %Q{Trackerific provides package tracking to Rails.}
19
- gem.description = %Q{Trackerific provides USPS, FedEx and UPS package tracking to Rails.}
19
+ gem.description = %Q{Package tracking made easy for Rails. Currently supported services include FedEx, UPS, and USPS.}
20
20
  gem.email = "travis.j.haynes@gmail.com"
21
21
  gem.authors = ["Travis Haynes"]
22
22
  gem.rubyforge_project = "trackerific"
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.5
1
+ 0.6.0
data/changelog ADDED
@@ -0,0 +1,9 @@
1
+ Mon Jun 27 2011 - Travis Haynes <travis.j.haynes@gmail.com>
2
+
3
+ * Added support for USPS City / State API for looking up a city and state by
4
+ its zipcode.
5
+ * Renamed required_options to required_parameters. (Makes much more sense.)
6
+ * Added a lot more specs for more code coverage.
7
+ * Changed the way that Details and Events are initialized. Using options
8
+ instead of parameters.
9
+ * Removed doc folder from git, since it is automatically generated with yard
@@ -18,8 +18,8 @@ module Trackerific
18
18
  unless args.empty?
19
19
  # Only accept Hashes
20
20
  raise ArgumentError unless args[0].class == Hash
21
- # Validate configuration values against the required options for that service
22
- validate_options args[0], Trackerific.service_get(sym).required_options
21
+ # Validate configuration values against the required parameters for that service
22
+ validate_options args[0], Trackerific.service_get(sym).required_parameters
23
23
  # Store the configuration options
24
24
  @options[sym] = args[0]
25
25
  end
@@ -2,18 +2,23 @@ module Trackerific
2
2
  # Details returned when tracking a package. Stores the package identifier,
3
3
  # a summary, and the events.
4
4
  class Details
5
+ include OptionsHelper
6
+
5
7
  # Provides a new instance of Details
6
- # @param [String] package_id the package identifier
7
- # @param [String] summary a summary of the tracking status
8
- # @param [Array, Trackerific::Event] events the tracking events
8
+ # @param [Hash] details The details for this package
9
9
  # @api private
10
- def initialize(package_id, summary, events)
11
- @package_id = package_id
12
- @summary = summary
13
- @events = events
10
+ def initialize(details = {})
11
+ required = [:package_id, :summary, :events]
12
+ valid = required + [:weight, :via]
13
+ validate_options(details, required, valid)
14
+ @package_id = details[:package_id]
15
+ @summary = details[:summary]
16
+ @events = details[:events]
17
+ @weight = details[:weight] || nil
18
+ @via = details[:via] || nil
14
19
  end
15
20
 
16
- # Read-only string for the package identifier
21
+ # The package identifier
17
22
  # @example Get the id of a tracked package
18
23
  # details.package_id # => the package identifier
19
24
  # @return [String] the package identifier
@@ -22,7 +27,7 @@ module Trackerific
22
27
  @package_id
23
28
  end
24
29
 
25
- # Read-only string for the summary of the package's tracking events
30
+ # Summary of the package's tracking events
26
31
  # @example Get the summary of a tracked package
27
32
  # details.summary # => Summary of the tracking events (i.e. Delivered)
28
33
  # @return [String] a summary of the tracking status
@@ -31,7 +36,7 @@ module Trackerific
31
36
  @summary
32
37
  end
33
38
 
34
- # Read-only string for the events for this package
39
+ # The events for this package
35
40
  # @example Print all the events for a tracked package
36
41
  # puts details.events
37
42
  # @example Get the date the package was shipped
@@ -45,5 +50,26 @@ module Trackerific
45
50
  def events
46
51
  @events
47
52
  end
53
+
54
+ # The weight of the package (may not be supported by all services)
55
+ # @example Get the weight of a package
56
+ # details.weight[:weight] # => the weight
57
+ # details.weight[:units] # => the units of measurement for the weight (i.e. "LBS")
58
+ # @return [Hash] Example: { units: 'LBS', weight: 19.1 }
59
+ # @api public
60
+ def weight
61
+ @weight
62
+ end
63
+
64
+ # Example: UPS 2ND DAY AIR. May not be supported by all services
65
+ # @example Get how the package was shipped
66
+ # ups = Trackerific::UPS.new :user_id => "userid"
67
+ # details = ups.track_package "1Z12345E0291980793"
68
+ # details.via # => "UPS 2ND DAY AIR"
69
+ # @return [String] The service used to ship the package
70
+ # @api public
71
+ def via
72
+ @via
73
+ end
48
74
  end
49
75
  end
@@ -1,15 +1,17 @@
1
1
  module Trackerific
2
2
  # Provides details for a tracking event
3
3
  class Event
4
+ include OptionsHelper
5
+
4
6
  # Provides a new instance of Event
5
- # @param [DateTime] date the date / time of the event
6
- # @param [String] description the event's description
7
- # @param [String] location where the event took place
7
+ # @param [Hash] details The details of the event
8
8
  # @api private
9
- def initialize(date, description, location)
10
- @date = date
11
- @description = description
12
- @location = location
9
+ def initialize(details = {})
10
+ required_details = [:date, :description, :location]
11
+ validate_options details, required_details
12
+ @date = details[:date]
13
+ @description = details[:description]
14
+ @location = details[:location]
13
15
  end
14
16
 
15
17
  # The date and time of the event
@@ -3,10 +3,10 @@ module Trackerific
3
3
  class Service
4
4
  include OptionsHelper
5
5
 
6
- # Creates a new instance of Trackerific::Service with required options
6
+ # Creates a new instance of Trackerific::Service
7
7
  # @api private
8
8
  def initialize(options = {})
9
- validate_options options, self.class.required_options
9
+ validate_options options, self.class.required_parameters, self.class.valid_options
10
10
  @options = options
11
11
  end
12
12
 
@@ -48,21 +48,37 @@ module Trackerific
48
48
  end
49
49
 
50
50
  # An array of options that are required to create a new instance of this class
51
- # @return [Array] the required options
51
+ # @return [Array] the required parameters
52
52
  # @example Override this method in your custom tracking service to enforce some options
53
53
  # module Trackerific
54
54
  # class MyTrackingService < Service
55
- # def self.required_options
55
+ # def self.required_parameters
56
56
  # [:all, :these, :are, :required]
57
57
  # end
58
58
  # end
59
59
  # end
60
60
  # @api semipublic
61
- def required_options
61
+ def required_parameters
62
62
  []
63
63
  end
64
64
 
65
- # Provides a humanized string that provides the name of the service (i.e. "FedEx")
65
+ # An array of valid options used for creating this class
66
+ # @return [Array] the valid options
67
+ # @example Override this method in your custom tracking service to add options
68
+ # module Trackerific
69
+ # class MyTrackingService < Service
70
+ # def self.valid_options
71
+ # # NOTE: make sure to include the required parameters in this list!
72
+ # required_parameters + [:some, :more, :options]
73
+ # end
74
+ # end
75
+ # end
76
+ # @api semipublic
77
+ def valid_options
78
+ required_parameters + []
79
+ end
80
+
81
+ # Provides a humanized string that provides the name of the service
66
82
  # @return [String] the service name
67
83
  # @note This defaults to using the class name.
68
84
  # @example Override this method in your custom tracking service to provide a name
@@ -17,11 +17,11 @@ module Trackerific
17
17
  [ /^[0-9]{15}$/ ]
18
18
  end
19
19
 
20
- # Returns an Array of required options used when creating a new instance
21
- # @return [Array] required options for tracking a FedEx package are :account
20
+ # Returns an Array of required parameters used when creating a new instance
21
+ # @return [Array] required parameters for tracking a FedEx package are :account
22
22
  # and :meter
23
23
  # @api private
24
- def required_options
24
+ def required_parameters
25
25
  [:account, :meter]
26
26
  end
27
27
  end
@@ -52,13 +52,17 @@ module Trackerific
52
52
  date = Time.parse("#{e["Date"]} #{e["Time"]}")
53
53
  desc = e["Description"]
54
54
  addr = e["Address"]
55
- events << Trackerific::Event.new(date, desc, "#{addr["StateOrProvinceCode"]} #{addr["PostalCode"]}")
55
+ events << Trackerific::Event.new(
56
+ :date => date,
57
+ :description => desc,
58
+ :location => "#{addr["StateOrProvinceCode"]} #{addr["PostalCode"]}"
59
+ )
56
60
  end
57
61
  # Return a Trackerific::Details containing all the events
58
62
  Trackerific::Details.new(
59
- details["TrackingNumber"],
60
- details["StatusDescription"],
61
- events
63
+ :package_id => details["TrackingNumber"],
64
+ :summary => details["StatusDescription"],
65
+ :events => events
62
66
  )
63
67
  end
64
68
 
@@ -10,17 +10,14 @@ module Trackerific
10
10
  # @return [Array, Regexp] the regular expression
11
11
  # @api private
12
12
  def package_id_matchers
13
- if defined?(Rails)
14
- return [ /XXXXXXXXXX/, /XXXxxxxxxx/ ] unless Rails.env.production?
15
- else
16
- return [ ] # no matchers in production mode
17
- end
13
+ return [ /XXXXXXXXXX/, /XXXxxxxxxx/ ] unless Rails.env.production?
14
+ return [ ]
18
15
  end
19
16
 
20
- # Returns an Array of required options used when creating a new instance
21
- # @return [Array] required options
17
+ # Returns an Array of required parameters used when creating a new instance
18
+ # @return [Array] required parameters
22
19
  # @api private
23
- def required_options
20
+ def required_parameters
24
21
  [ ]
25
22
  end
26
23
  end
@@ -38,12 +35,24 @@ module Trackerific
38
35
  super
39
36
  if package_id == "XXXXXXXXXX"
40
37
  Trackerific::Details.new(
41
- package_id,
42
- "Your package was delivered.",
43
- [
44
- Trackerific::Event.new(Date.today, "Package delivered.", "SANTA MARIA, CA"),
45
- Trackerific::Event.new(Date.today - 1, "Package scanned.", "SANTA BARBARA, CA"),
46
- Trackerific::Event.new(Date.today - 2, "Package picked up for delivery.", "LOS ANGELES, CA")
38
+ :package_id => package_id,
39
+ :summary => "Your package was delivered.",
40
+ :events => [
41
+ Trackerific::Event.new(
42
+ :date => Date.today,
43
+ :description => "Package delivered.",
44
+ :location => "SANTA MARIA, CA"
45
+ ),
46
+ Trackerific::Event.new(
47
+ :date => Date.today - 1,
48
+ :description => "Package scanned.",
49
+ :location => "SANTA BARBARA, CA"
50
+ ),
51
+ Trackerific::Event.new(
52
+ :date => Date.today - 2,
53
+ :description => "Package picked up for delivery.",
54
+ :location => "LOS ANGELES, CA"
55
+ )
47
56
  ]
48
57
  )
49
58
  else
@@ -21,10 +21,10 @@ module Trackerific
21
21
  def package_id_matchers
22
22
  [ /^.Z/, /^[HK].{10}$/ ]
23
23
  end
24
- # The required options for tracking a UPS package
25
- # @return [Array] the required options for tracking a UPS package
24
+ # The required parameters for tracking a UPS package
25
+ # @return [Array] the required parameters for tracking a UPS package
26
26
  # @api private
27
- def required_options
27
+ def required_parameters
28
28
  [:key, :user_id, :password]
29
29
  end
30
30
  end
@@ -64,7 +64,7 @@ module Trackerific
64
64
  activity = [activity] if activity.is_a? Hash
65
65
  # UPS does not provide a summary, so we'll just use the last tracking status
66
66
  summary = activity.first['Status']['StatusType']['Description'].titleize
67
- details = []
67
+ events = []
68
68
  activity.each do |a|
69
69
  # the time format from UPS is HHMMSS, which cannot be directly converted
70
70
  # to a Ruby time.
@@ -75,13 +75,17 @@ module Trackerific
75
75
  date = DateTime.parse("#{date} #{hours}:#{minutes}:#{seconds}")
76
76
  desc = a['Status']['StatusType']['Description'].titleize
77
77
  loc = a['ActivityLocation']['Address'].map {|k,v| v}.join(" ")
78
- details << Trackerific::Event.new(date, desc, loc)
78
+ events << Trackerific::Event.new(
79
+ :date => date,
80
+ :description => desc,
81
+ :location => loc
82
+ )
79
83
  end
80
84
 
81
85
  Trackerific::Details.new(
82
- @package_id,
83
- summary,
84
- details
86
+ :package_id => @package_id,
87
+ :summary => summary,
88
+ :events => events
85
89
  )
86
90
  end
87
91
 
@@ -1,4 +1,5 @@
1
1
  require 'date'
2
+ require 'active_support/core_ext/object/to_query'
2
3
 
3
4
  module Trackerific
4
5
  require 'builder'
@@ -19,12 +20,19 @@ module Trackerific
19
20
  [ /^E\D{1}\d{9}\D{2}$|^9\d{15,21}$/ ]
20
21
  end
21
22
 
22
- # The required options for tracking a UPS package
23
- # @return [Array] the required options for tracking a UPS package
23
+ # The required parameters for tracking a UPS package
24
+ # @return [Array] the required parameters for tracking a UPS package
24
25
  # @api private
25
- def required_options
26
+ def required_parameters
26
27
  [:user_id]
27
28
  end
29
+
30
+ # List of all valid options for tracking a UPS package
31
+ # @return [Array] the valid options for tracking a UPS package
32
+ # @api private
33
+ def valid_options
34
+ required_parameters + [:use_city_state_lookup]
35
+ end
28
36
  end
29
37
 
30
38
  # Tracks a USPS package
@@ -42,44 +50,129 @@ module Trackerific
42
50
  Rails.env.production? ? "/ShippingAPI.dll" : "/ShippingAPITest.dll",
43
51
  :query => {
44
52
  :API => 'TrackV2',
45
- :XML => build_xml_request
53
+ :XML => build_tracking_xml_request
46
54
  }.to_query
47
55
  )
48
- # throw any HTTP errors
49
- response.error! unless response.code == 200
50
- # raise a Trackerific::Error if there is an error in the response, or if the
51
- # tracking response is malformed
52
- raise Trackerific::Error, response['Error']['Description'] unless response['Error'].nil?
53
- raise Trackerific::Error, "Tracking information not found in response from server." if response['TrackResponse'].nil?
56
+ # raise any errors
57
+ error = check_response_for_errors(response, :TrackV2)
58
+ raise error unless error.nil?
54
59
  # get the tracking information from the response, and convert into a
55
60
  # Trackerific::Details
56
61
  tracking_info = response['TrackResponse']['TrackInfo']
57
- details = []
62
+ events = []
63
+ # check if we should look up the exact location of the details
64
+ use_city_state_lookup = @options[:use_city_state_lookup] || false
65
+ # parse the details
58
66
  tracking_info['TrackDetail'].each do |d|
59
67
  # each tracking detail is a string in this format:
60
- # MM DD HH:MM am/pm DESCRIPTION CITY STATE ZIP.
61
- # unfortunately, it will not be possible to tell the difference between
62
- # the location, and the summary. So, for USPS, the location will be in
63
- # the summary
68
+ # MM DD HH:MM am/pm DESCRIPTION CITY STATE ZIP
64
69
  d = d.split(" ")
65
70
  date = DateTime.parse(d[0..3].join(" "))
66
71
  desc = d[4..d.length].join(" ")
67
- details << Trackerific::Event.new(date, desc, "")
72
+ # the zip code is always the last word, if it is all numbers
73
+ if use_city_state_lookup then
74
+ # this gets the exact location of the package, and is very accurate,
75
+ # however, it requires access to the shipping services in USPS
76
+ zip = d[d.length-1]
77
+ loc = ""
78
+ # check if zip is a number
79
+ if zip.to_i.to_s == zip
80
+ loc = city_state_lookup(zip)
81
+ loc = "#{loc[:city].titelize}, #{loc[:state]} #{loc[:zip]}"
82
+ # attempt to delete the location from the description
83
+ desc = desc.gsub("#{loc[:city]} #{loc[:state]} #{loc[:zip]}", "")
84
+ end
85
+ else
86
+ # extract the location from the description - not always accurate,
87
+ # but better than nothing
88
+ d = desc.split(" ") # => ['the', 'description', 'city', 'state', 'zip']
89
+ desc = d[0..d.length-4].join(" ") # => "the description"
90
+ loc = d[d.length-3, d.length] # => ['city', 'state', 'zip']
91
+ loc = "#{loc[0].titleize}, #{loc[1]} #{loc[2]}" # "City, STATE zip"
92
+ end
93
+ events << Trackerific::Event.new(
94
+ :date => date,
95
+ :description => desc.capitalize,
96
+ :location => loc
97
+ )
68
98
  end unless tracking_info['TrackDetail'].nil?
69
99
  # return the details
70
100
  Trackerific::Details.new(
71
- tracking_info['ID'],
72
- tracking_info['TrackSummary'],
73
- details
101
+ :package_id => tracking_info['ID'],
102
+ :summary => tracking_info['TrackSummary'],
103
+ :events => events
74
104
  )
75
105
  end
76
106
 
107
+ # Gets the city/state of a zipcode
108
+ # @param [String] zipcode The zipcode to find the city/state for
109
+ # @return [Hash] { zip: 'the zipcode, 'city: "the city", state: "the state" }
110
+ # @example Lookup zipcode for Beverly Hills, CA
111
+ # usps = Trackerific::USPS.new :user_id => 'youruserid'
112
+ # city_state = usps.city_state_lookup(90210)
113
+ # city_state[:city] # => BEVERLY HILLS
114
+ # city_state[:state] # => CA
115
+ # city_state[:zip] # => 90210
116
+ # @api public
117
+ def city_state_lookup(zipcode)
118
+ response = self.class.get(
119
+ Rails.env.production? ? "/ShippingAPI.dll" : "/ShippingAPITest.dll",
120
+ :query => {
121
+ :API => 'CityStateLookup',
122
+ :XML => build_city_state_xml_request(zipcode)
123
+ }.to_query
124
+ )
125
+ # raise any errors
126
+ error = check_response_for_errors(response, :CityStateLookup)
127
+ raise error unless error.nil?
128
+ # return the city, state, and zip
129
+ response = response['CityStateLookupResponse']['ZipCode']
130
+ {
131
+ :city => response['City'],
132
+ :state => response['State'],
133
+ :zip => response['Zip5']
134
+ }
135
+ end
136
+
77
137
  protected
78
138
 
79
- # Builds an XML request to send to USPS
139
+ # Checks a HTTParty response for USPS, or HTTP errors
140
+ # @param [HTTParty::Response] response The HTTParty response to check
141
+ # @return The exception to raise, or nil
142
+ # @api private
143
+ def check_response_for_errors(response, api)
144
+ # return any HTTP errors
145
+ return response.error unless response.code == 200
146
+ # return a Trackerific::Error if there is an error in the response, or if
147
+ # the tracking response is malformed
148
+ return Trackerific::Error.new(response['Error']['Description']) unless response['Error'].nil?
149
+ return Trackerific::Error.new("Tracking information not found in response from server.") if response['TrackResponse'].nil? && api == :TrackV2
150
+ return Trackerific::Error.new("City / state information not found in response from server.") if response['CityStateLookupResponse'].nil? && api == :CityStateLookup
151
+ return nil # no errors to report
152
+ end
153
+
154
+ # Builds an XML city/state lookup request
155
+ # @param [String] zipcode The zipcode to find the city/state for
156
+ # @return [String] the xml request
157
+ # @api private
158
+ def build_city_state_xml_request(zipcode)
159
+ xml = ""
160
+ # set up the Builder
161
+ builder = ::Builder::XmlMarkup.new(:target => xml)
162
+ # add the XML header
163
+ builder.instruct! :xml, :version => "1.0", :encoding => "UTF-8"
164
+ # build the request
165
+ builder.CityStateLookupRequest(:USERID => @options[:user_id]) do |request|
166
+ request.ZipCode(:ID => "5") do |zip|
167
+ zip.Zip5 zipcode
168
+ end
169
+ end
170
+ end
171
+
172
+ # Builds an XML tracking request
80
173
  # @return [String] the xml request
81
174
  # @api private
82
- def build_xml_request
175
+ def build_tracking_xml_request
83
176
  xml = ""
84
177
  # set up the Builder
85
178
  builder = ::Builder::XmlMarkup.new(:target => xml)
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0"?>
2
+ <CityStateLookupResponse>
3
+ <ZipCode ID="0">
4
+ <Zip5>90210</Zip5>
5
+ <City>BEVERLY HILLS</City>
6
+ <State>CA</State>
7
+ </ZipCode>
8
+ </CityStateLookupResponse>