awesome_usps 0.5.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,236 @@
1
+ module AwesomeUsps
2
+ module Shipping
3
+
4
+ MAX_RETRIES = 3
5
+
6
+ ORIGIN_ZIP = "07024" #User should change this
7
+
8
+ LIVE_DOMAIN = 'production.shippingapis.com'
9
+ LIVE_RESOURCE = '/ShippingAPI.dll'
10
+
11
+ TEST_DOMAINS = { #indexed by security; e.g. TEST_DOMAINS[USE_SSL[:rates]]
12
+ true => 'secure.shippingapis.com',
13
+ false => 'testing.shippingapis.com'
14
+ }
15
+
16
+ TEST_RESOURCE = '/ShippingAPITest.dll'
17
+
18
+ API_CODES = {
19
+ :us_rates => 'RateV3',
20
+ :world_rates => 'IntlRate',
21
+ :test => 'CarrierPickupAvailability'
22
+ }
23
+
24
+ USE_SSL = {
25
+ :tracking => false,
26
+ :test => true
27
+ }
28
+ CONTAINERS = {
29
+ :envelope => 'Flat Rate Envelope',
30
+ :box => 'Flat Rate Box'
31
+ }
32
+ MAIL_TYPES = {
33
+ :package => 'Package',
34
+ :postcard => 'Postcards or aerogrammes',
35
+ :matter_for_the_blind => 'Matter for the blind',
36
+ :envelope => 'Envelope'
37
+ }
38
+ PACKAGE_PROPERTIES = {
39
+ 'ZipOrigination' => :origin_zip,
40
+ 'ZipDestination' => :destination_zip,
41
+ 'Pounds' => :pounds,
42
+ 'Ounces' => :ounces,
43
+ 'Container' => :container,
44
+ 'Size' => :size,
45
+ 'Machinable' => :machinable,
46
+ 'Zone' => :zone,
47
+ 'Postage' => :postage,
48
+ 'Restrictions' => :restrictions
49
+ }
50
+ POSTAGE_PROPERTIES = {
51
+ 'MailService' => :service,
52
+ 'Rate' => :rate
53
+ }
54
+ US_SERVICES = {
55
+ :first_class => 'FIRST CLASS',
56
+ :priority => 'PRIORITY',
57
+ :express => 'EXPRESS',
58
+ :bpm => 'BPM',
59
+ :parcel => 'PARCEL',
60
+ :media => 'MEDIA',
61
+ :library => 'LIBRARY',
62
+ :all => 'ALL'
63
+ }
64
+
65
+ #Determins size of package automatically. Taken from Active_Shipping
66
+ def size_code_for(package)
67
+ total = package.inches(:length) + package.inches(:girth)
68
+ if total <= 84
69
+ return 'REGULAR'
70
+ elsif total <= 108
71
+ return 'LARGE'
72
+ else # <= 130
73
+ return 'OVERSIZE'
74
+ end
75
+ end
76
+
77
+ # options Are?
78
+ def domestic_rates(destination_zip, packages, options={})
79
+ @packages = Array(packages)
80
+ @destination_zip = destination_zip
81
+ @options = options
82
+ request = xml_for_us
83
+ tracking_commit(:us_rates, request ,false)
84
+ end
85
+
86
+ # options Are?
87
+ def world_rates(country, packages, options={})
88
+ @packages = Array(packages)
89
+ @country = country
90
+ @options = options
91
+ request = xml_for_world
92
+ tracking_commit(:world_rates, request ,false)
93
+ end
94
+
95
+ private
96
+ # XML built with Build:XmlMarkup
97
+ def xml_for_us
98
+ xm = Builder::XmlMarkup.new
99
+ xm.RateV3Request("USERID"=>"#{@username}") do
100
+ @packages.each_index do |id|
101
+ p = @packages[id]
102
+ xm.Package("ID" => "#{id}") {
103
+ xm.Service("#{US_SERVICES[@options[:service]] || :all}")
104
+ xm.ZipOrigination(ORIGIN_ZIP)
105
+ xm.ZipDestination(@destination_zip)
106
+ xm.Pounds("0")
107
+ xm.Ounces("#{'%0.1f' % [p.ounces,1].max}")
108
+ if p.options[:container] and [nil,:all,:express,:priority].include? p.service
109
+ xm.Container(CONTAINERS[p.options[:container]])
110
+ end
111
+ xm.Size(size_code_for(p))
112
+ xm.Width(p.inches(:width))
113
+ xm.Length(p.inches(:length))
114
+ xm.Height(p.inches(:height))
115
+ xm.Girth(p.inches(:girth))
116
+ xm.Machinable((p.options[:machinable] ? true : false).to_s.upcase)
117
+ }
118
+ end
119
+ end
120
+ end
121
+
122
+ # XML built with Build:XmlMarkup
123
+ def xml_for_world
124
+ xm = Builder::XmlMarkup.new
125
+ xm.IntlRateRequest("USERID"=>"#{@username}") do
126
+ @packages.each_index do |id|
127
+ p = @packages[id]
128
+ xm.Package("ID" => "#{id}") {
129
+ xm.Pounds("0")
130
+ xm.Ounces("#{'%0.1f' % [p.ounces,1].max.ceil}")
131
+ xm.MailType("#{MAIL_TYPES[p.options[:mail_type]] || 'Package'}")
132
+ xm.ValueOfContents(p.value / 100.0) if p.value && p.currency == 'USD'
133
+ xm.Country(@country)
134
+ }
135
+ end
136
+ end
137
+ end
138
+
139
+
140
+ # Returns the sent xml as a hash orgainzied by each package and service type.
141
+ # example
142
+ # {Package1 =>{'First Class' => "1.90"}Package2 => {'First Class' => "26.90" }
143
+
144
+ def parse_us(xml)
145
+ domestic_rate_hash = Hash.new
146
+ i= 0
147
+ Hpricot.parse(xml).search('package').each do |package|
148
+ i+=1
149
+ #This will return the first error description found in response xml.
150
+ #TODO find way to return all errors.
151
+ if package.search("error") != []
152
+ RAILS_DEFAULT_LOGGER.info("package number #{i} has the error #{package.search("description").inner_html} please fix before continuing")
153
+
154
+ return "package number #{i} has the error #{package.search("description").inner_html} please fix before continuing"
155
+ end
156
+ #Initializing hash for each package. Is there a better way I wonder.
157
+ domestic_rate_hash["Package#{i}"] = {}
158
+ #Going through each package and finding the rate.
159
+ package.search("postage").each do |services|
160
+ mailservice=services.search("mailservice")
161
+ rate = services.search("rate")
162
+ domestic_rate_hash["Package#{i}"][mailservice.inner_html] = rate.inner_html
163
+ end
164
+ end
165
+ if domestic_rate_hash == {}
166
+ domestic_rate_hash = Hpricot.parse(xml).search('description').inner_html
167
+ end
168
+ return domestic_rate_hash
169
+ end
170
+
171
+ def parse_world(xml)
172
+ international_rate_hash = Hash.new
173
+ i= 0
174
+ Hpricot.parse(xml).search('package').each do |package|
175
+ i+=1
176
+ #This will return the first error description found in response xml.
177
+ #TODO find way to return all errors.
178
+ if package.search("error") != []
179
+ RAILS_DEFAULT_LOGGER.info("package number #{i} has the error #{package.search("description").inner_html} please fix before continuing")
180
+
181
+ return "package number #{i} has the error #{package.search("description").inner_html} please fix before continuing"
182
+ end
183
+ #Initializing hash for each package. Is there a better way I wonder.
184
+ international_rate_hash["Package#{i}"] = {}
185
+ #Going through each package and finding the rate.
186
+ package.search("service").each do |services|
187
+ svcdescription=services.search("svcdescription")
188
+ rate = services.search("postage")
189
+ international_rate_hash["Package#{i}"][svcdescription.inner_html] = rate.inner_html
190
+ end
191
+ end
192
+ if international_rate_hash == {}
193
+ international_rate_hash = Hpricot.parse(xml).search('description').inner_html
194
+ end
195
+ return international_rate_hash
196
+ end
197
+
198
+ def tracking_commit(action, request, test = false)
199
+ retries = MAX_RETRIES
200
+ begin
201
+ url = URI.parse("http://#{LIVE_DOMAIN}#{LIVE_RESOURCE}")
202
+ req = Net::HTTP::Post.new(url.path)
203
+ req.set_form_data({'API' => API_CODES[action], 'XML' => request})
204
+ response = Net::HTTP.new(url.host, url.port)
205
+ response.open_timeout = 5
206
+ response.read_timeout = 5
207
+ response.start
208
+ rescue Timeout::Error
209
+ if retries > 0
210
+ retries -= 1
211
+ retry
212
+ else
213
+ RAILS_DEFAULT_LOGGER.warn "The connection to the remote server timed out"
214
+ return "We appoligize for the inconvience but our USPS service is busy at the moment. To retry please refresh the browser"
215
+ end
216
+ rescue SocketError
217
+ RAILS_DEFAULT_LOGGER.error "There is a socket error with USPS plugin"
218
+ return "We appoligize for the inconvience but there is a problem with our server. To retry please refresh the browser"
219
+ end
220
+
221
+ response = response.request(req)
222
+ case response
223
+ when Net::HTTPSuccess, Net::HTTPRedirection
224
+ if (action == :us_rates)
225
+ parse_us(response.body)
226
+ else
227
+ parse_world(response.body)
228
+ end
229
+ else
230
+ RAILS_DEFAULT_LOGGER.warn("USPS plugin settings are wrong #{response}")
231
+ return "USPS plugin settings are wrong #{response}"
232
+ end
233
+ end
234
+ end
235
+ end
236
+
@@ -0,0 +1,88 @@
1
+ module AwesomeUsps
2
+ module Tracking
3
+ MAX_RETRIES = 3
4
+
5
+ LIVE_DOMAIN = 'production.shippingapis.com'
6
+ LIVE_RESOURCE = '/ShippingAPI.dll'
7
+
8
+ TEST_DOMAIN ='testing.shippingapis.com'
9
+ TEST_RESOURCE = '/ShippingAPITest.dll'
10
+
11
+ API_CODE ='TrackV2'
12
+
13
+ # Takes your package tracking number and returns information for the USPS web API
14
+ def track(tracking_number)
15
+ @tracking_number = tracking_number
16
+ request = xml_for_tracking
17
+ commit(:tracking, request ,false)
18
+ end
19
+
20
+ def canned_tracking
21
+ @tracking_number = "EJ958083578US"
22
+ request = xml_for_tracking
23
+ commit(:tracking, request ,true)
24
+ end
25
+
26
+ # XML from a straight string.
27
+ # "<TrackFieldRequest USERID='#{@username}'><TrackID ID='#{@tracking_number}'></TrackID></TrackFieldRequest>"
28
+ def xml_for_tracking
29
+ xm = Builder::XmlMarkup.new
30
+ xm.TrackFieldRequest("USERID" =>"#{@username}") do
31
+ xm.TrackID("ID"=> "#{@tracking_number}")
32
+ end
33
+ end
34
+
35
+ # Parses the XML into an array broken up by each event.
36
+ # Example of returned array
37
+ def parse(xml)
38
+ event_list = []
39
+ parse = Hpricot.parse(xml)/:trackdetail
40
+ if parse == []
41
+ RAILS_DEFAULT_LOGGER.info "#{xml}"
42
+ return (Hpricot.parse(xml)/:description).inner_html
43
+ else
44
+ parse.each do |detail|
45
+ h = {}
46
+ detail.children.each { |elem| h[elem.name.to_sym] = elem.inner_text unless elem.inner_text.blank? }
47
+ event_list << h
48
+ end
49
+ end
50
+ event_list
51
+ end
52
+
53
+ private
54
+ def commit(action, request, test = false)
55
+ retries = MAX_RETRIES
56
+ begin
57
+ url = URI.parse(test ? "http://#{TEST_DOMAIN}#{TEST_RESOURCE}" : "http://#{LIVE_DOMAIN}#{LIVE_RESOURCE}")
58
+ req = Net::HTTP::Post.new(url.path)
59
+ req.set_form_data({'API' => API_CODE, 'XML' => request})
60
+ response = Net::HTTP.new(url.host, url.port)
61
+ response.open_timeout = 5
62
+ response.read_timeout = 5
63
+ response.start
64
+ rescue Timeout::Error
65
+ if retries > 0
66
+ retries -= 1
67
+ retry
68
+ else
69
+ RAILS_DEFAULT_LOGGER.warn "The connection to the remote server timed out"
70
+ return "We appoligize for the inconvience but our USPS service is busy at the moment. To retry please refresh the browser"
71
+
72
+ end
73
+ rescue SocketError
74
+ RAILS_DEFAULT_LOGGER.error "There is a socket error with USPS plugin"
75
+ return "We appoligize for the inconvience but there is a problem with our server. To retry please refresh the browser"
76
+ end
77
+
78
+ response = response.request(req)
79
+ case response
80
+ when Net::HTTPSuccess, Net::HTTPRedirection
81
+ parse(response.body)
82
+ else
83
+ RAILS_DEFAULT_LOGGER.warn("USPS plugin settings are wrong #{response}")
84
+ end
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,9 @@
1
+ module AwesomeUsps
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 5
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/awesome_usps.rb'}"
9
+ puts "Loading awesome_usps gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ GEM_NAME = 'awesome_usps' # what ppl will type to install your gem
4
+ RUBYFORGE_PROJECT = 'awesome_usps'
5
+
6
+ require 'rubygems'
7
+ begin
8
+ require 'newgem'
9
+ require 'rubyforge'
10
+ rescue LoadError
11
+ puts "\n\nGenerating the website requires the newgem RubyGem"
12
+ puts "Install: gem install newgem\n\n"
13
+ exit(1)
14
+ end
15
+ require 'redcloth'
16
+ require 'syntax/convertors/html'
17
+ require 'erb'
18
+ require File.dirname(__FILE__) + "/../lib/#{GEM_NAME}/version.rb"
19
+
20
+ version = AwesomeUsps::VERSION::STRING
21
+ download = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
22
+
23
+ def rubyforge_project_id
24
+ RubyForge.new.autoconfig["group_ids"][RUBYFORGE_PROJECT]
25
+ end
26
+
27
+ class Fixnum
28
+ def ordinal
29
+ # teens
30
+ return 'th' if (10..19).include?(self % 100)
31
+ # others
32
+ case self % 10
33
+ when 1: return 'st'
34
+ when 2: return 'nd'
35
+ when 3: return 'rd'
36
+ else return 'th'
37
+ end
38
+ end
39
+ end
40
+
41
+ class Time
42
+ def pretty
43
+ return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}"
44
+ end
45
+ end
46
+
47
+ def convert_syntax(syntax, source)
48
+ return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^<pre>|</pre>$!,'')
49
+ end
50
+
51
+ if ARGV.length >= 1
52
+ src, template = ARGV
53
+ template ||= File.join(File.dirname(__FILE__), '/../website/template.html.erb')
54
+ else
55
+ puts("Usage: #{File.split($0).last} source.txt [template.html.erb] > output.html")
56
+ exit!
57
+ end
58
+
59
+ template = ERB.new(File.open(template).read)
60
+
61
+ title = nil
62
+ body = nil
63
+ File.open(src) do |fsrc|
64
+ title_text = fsrc.readline
65
+ body_text_template = fsrc.read
66
+ body_text = ERB.new(body_text_template).result(binding)
67
+ syntax_items = []
68
+ body_text.gsub!(%r!<(pre|code)[^>]*?syntax=['"]([^'"]+)[^>]*>(.*?)</\1>!m){
69
+ ident = syntax_items.length
70
+ element, syntax, source = $1, $2, $3
71
+ syntax_items << "<#{element} class='syntax'>#{convert_syntax(syntax, source)}</#{element}>"
72
+ "syntax-temp-#{ident}"
73
+ }
74
+ title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip
75
+ body = RedCloth.new(body_text).to_html
76
+ body.gsub!(%r!(?:<pre><code>)?syntax-temp-(\d+)(?:</code></pre>)?!){ syntax_items[$1.to_i] }
77
+ end
78
+ stat = File.stat(src)
79
+ created = stat.ctime
80
+ modified = stat.mtime
81
+
82
+ $stdout << template.result(binding)