awesome_usps 0.5.0

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