ideaoforder-shipping 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,251 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+ require 'rake/gempackagetask'
6
+ require 'rake/contrib/rubyforgepublisher'
7
+ require File.dirname(__FILE__) + '/lib/shipping'
8
+
9
+ PKG_VERSION = Shipping::VERSION
10
+ PKG_NAME = "shipping"
11
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
12
+ RUBY_FORGE_PROJECT = "shipping"
13
+ RUBY_FORGE_USER = ENV['RUBY_FORGE_USER'] || "cardmagic"
14
+ RELEASE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
15
+
16
+ PKG_FILES = FileList[
17
+ "lib/**/*", "bin/*", "test/**/*", "[A-Z]*", "Rakefile", "doc/**/*"
18
+ ]
19
+
20
+ desc "Default Task"
21
+ task :default => [ :test ]
22
+
23
+ # Run the unit tests
24
+ desc "Run all unit tests"
25
+ Rake::TestTask.new("test") { |t|
26
+ t.libs << "lib"
27
+ t.pattern = 'test/*/*_test.rb'
28
+ t.verbose = true
29
+ }
30
+
31
+ # Make a console, useful when working on tests
32
+ desc "Generate a test console"
33
+ task :console do
34
+ verbose( false ) { sh "irb -I lib/ -r 'shipping'" }
35
+ end
36
+
37
+ # Genereate the RDoc documentation
38
+ desc "Create documentation"
39
+ Rake::RDocTask.new("doc") { |rdoc|
40
+ rdoc.title = "Ruby Shipping - UPS, FedEx, USPS"
41
+ rdoc.rdoc_dir = 'html'
42
+ rdoc.rdoc_files.include('README')
43
+ rdoc.rdoc_files.include('lib/**/*.rb')
44
+ }
45
+
46
+ # Genereate the package
47
+ spec = Gem::Specification.new do |s|
48
+
49
+ #### Basic information.
50
+
51
+ s.name = 'shipping'
52
+ s.version = PKG_VERSION
53
+ s.summary = <<-EOF
54
+ A general shipping module to find out the shipping prices via UPS or FedEx.
55
+ EOF
56
+ s.description = <<-EOF
57
+ A general shipping module to find out the shipping prices via UPS or FedEx.
58
+ EOF
59
+
60
+ #### Which files are to be included in this gem? Everything! (Except CVS directories.)
61
+
62
+ s.files = PKG_FILES
63
+
64
+ #### Load-time details: library and application (you will need one or both).
65
+
66
+ s.add_dependency('builder', '>= 1.2.0')
67
+ s.requirements << "An xml-builder library."
68
+
69
+ s.require_path = 'lib'
70
+ s.autorequire = 'shipping'
71
+
72
+ #### Documentation and testing.
73
+
74
+ s.has_rdoc = true
75
+
76
+ #### Author and project details.
77
+
78
+ s.author = "Lucas Carlson"
79
+ s.email = "lucas@rufy.com"
80
+ s.homepage = "http://shipping.rufy.com/"
81
+ end
82
+
83
+ Rake::GemPackageTask.new(spec) do |pkg|
84
+ pkg.need_zip = true
85
+ pkg.need_tar = true
86
+ end
87
+
88
+ desc "Report code statistics (KLOCs, etc) from the application"
89
+ task :stats do
90
+ require 'code_statistics'
91
+ CodeStatistics.new(
92
+ ["Library", "lib"],
93
+ ["Units", "test"]
94
+ ).to_s
95
+ end
96
+
97
+ desc "Publish new documentation"
98
+ task :publish do
99
+ Rake::RubyForgePublisher.new('shipping', 'cardmagic').upload
100
+ `ssh rufy update-shipping-doc`
101
+ end
102
+
103
+ desc "Publish the release files to RubyForge."
104
+ task :upload => [:package] do
105
+ files = ["gem", "tgz", "zip"].map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
106
+
107
+ if RUBY_FORGE_PROJECT then
108
+ require 'net/http'
109
+ require 'open-uri'
110
+
111
+ project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
112
+ project_data = open(project_uri) { |data| data.read }
113
+ group_id = project_data[/[?&]group_id=(\d+)/, 1]
114
+ raise "Couldn't get group id" unless group_id
115
+
116
+ # This echos password to shell which is a bit sucky
117
+ if ENV["RUBY_FORGE_PASSWORD"]
118
+ password = ENV["RUBY_FORGE_PASSWORD"]
119
+ else
120
+ password = Proc.new do
121
+ sync = STDOUT.sync
122
+ begin
123
+ echo false
124
+ STDOUT.sync = true
125
+ print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
126
+ STDIN.gets.chomp
127
+ ensure
128
+ echo true
129
+ STDOUT.sync = sync
130
+ puts
131
+ end
132
+ end.call
133
+ end
134
+
135
+ login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
136
+ data = [
137
+ "login=1",
138
+ "form_loginname=#{RUBY_FORGE_USER}",
139
+ "form_pw=#{password}"
140
+ ].join("&")
141
+ http.post("/account/login.php", data)
142
+ end
143
+
144
+ cookie = login_response["set-cookie"]
145
+ raise "Login failed" unless cookie
146
+ headers = { "Cookie" => cookie }
147
+
148
+ release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
149
+ release_data = open(release_uri, headers) { |data| data.read }
150
+ package_id = release_data[/[?&]package_id=(\d+)/, 1]
151
+ raise "Couldn't get package id" unless package_id
152
+
153
+ first_file = true
154
+ release_id = ""
155
+
156
+ files.each do |filename|
157
+ basename = File.basename(filename)
158
+ file_ext = File.extname(filename)
159
+ file_data = File.open(filename, "rb") { |file| file.read }
160
+
161
+ puts "Releasing #{basename}..."
162
+
163
+ release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
164
+ release_date = Time.now.strftime("%Y-%m-%d %H:%M")
165
+ type_map = {
166
+ ".zip" => "3000",
167
+ ".tgz" => "3110",
168
+ ".gz" => "3110",
169
+ ".gem" => "1400"
170
+ }; type_map.default = "9999"
171
+ type = type_map[file_ext]
172
+ boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
173
+
174
+ query_hash = if first_file then
175
+ {
176
+ "group_id" => group_id,
177
+ "package_id" => package_id,
178
+ "release_name" => RELEASE_NAME,
179
+ "release_date" => release_date,
180
+ "type_id" => type,
181
+ "processor_id" => "8000", # Any
182
+ "release_notes" => "",
183
+ "release_changes" => "",
184
+ "preformatted" => "1",
185
+ "submit" => "1"
186
+ }
187
+ else
188
+ {
189
+ "group_id" => group_id,
190
+ "release_id" => release_id,
191
+ "package_id" => package_id,
192
+ "step2" => "1",
193
+ "type_id" => type,
194
+ "processor_id" => "8000", # Any
195
+ "submit" => "Add This File"
196
+ }
197
+ end
198
+
199
+ query = "?" + query_hash.map do |(name, value)|
200
+ [name, URI.encode(value)].join("=")
201
+ end.join("&")
202
+
203
+ data = [
204
+ "--" + boundary,
205
+ "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
206
+ "Content-Type: application/octet-stream",
207
+ "Content-Transfer-Encoding: binary",
208
+ "", file_data, ""
209
+ ].join("\x0D\x0A")
210
+
211
+ release_headers = headers.merge(
212
+ "Content-Type" => "multipart/form-data; boundary=#{boundary}"
213
+ )
214
+
215
+ target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
216
+ http.post(target + query, data, release_headers)
217
+ end
218
+
219
+ if first_file then
220
+ release_id = release_response.body[/release_id=(\d+)/, 1]
221
+ raise("Couldn't get release id") unless release_id
222
+ end
223
+
224
+ first_file = false
225
+ end
226
+ end
227
+ end
228
+
229
+ begin
230
+ if !defined?(USE_TERMIOS) || USE_TERMIOS
231
+ require 'termios'
232
+ else
233
+ raise LoadError
234
+ end
235
+
236
+ # Enable or disable stdin echoing to the terminal.
237
+ def echo(enable)
238
+ term = Termios::getattr(STDIN)
239
+
240
+ if enable
241
+ term.c_lflag |= (Termios::ECHO | Termios::ICANON)
242
+ else
243
+ term.c_lflag &= ~Termios::ECHO
244
+ end
245
+
246
+ Termios::setattr(STDIN, Termios::TCSANOW, term)
247
+ end
248
+ rescue LoadError
249
+ def echo(enable)
250
+ end
251
+ end
@@ -0,0 +1,314 @@
1
+ # Author:: Lucas Carlson (mailto:lucas@rufy.com)
2
+ # Copyright:: Copyright (c) 2005 Lucas Carlson
3
+ # License:: LGPL
4
+
5
+ # Updated:: 12-22-2008 by Mark Dickson (mailto:mark@sitesteaders.com)
6
+
7
+ module Shipping
8
+ VERSION = "1.6.0"
9
+
10
+ class ShippingError < StandardError; end
11
+ class ShippingRequiredFieldError < StandardError; end
12
+
13
+ class Base
14
+ attr_reader :data, :response, :plain_response, :required, :services
15
+
16
+ attr_writer :ups_license_number, :ups_shipper_number, :ups_user, :ups_password, :ups_url, :ups_tool
17
+ attr_writer :fedex_account, :fedex_meter, :fedex_url, :fedex_package_weight_limit_in_lbs
18
+
19
+ attr_accessor :name, :phone, :company, :email, :address, :address2, :city, :state, :zip, :country
20
+ attr_accessor :sender_name, :sender_phone, :sender_company, :sender_email, :sender_address, :sender_city, :sender_state, :sender_zip, :sender_country
21
+
22
+ attr_accessor :weight, :weight_units, :insured_value, :declared_value, :transaction_type, :description
23
+ attr_accessor :measure_units, :measure_length, :measure_width, :measure_height
24
+ attr_accessor :package_total, :packaging_type, :service_type
25
+
26
+ attr_accessor :price, :discount_price, :eta, :time_in_transit
27
+
28
+ attr_accessor :ship_date, :dropoff_type, :pay_type, :currency_code, :image_type, :label_type
29
+
30
+ attr_accessor :weight_each, :quantity, :max_weight, :max_quantity, :items
31
+
32
+ def initialize(options = {})
33
+ prefs = File.expand_path(options[:prefs] || "~/.shipping.yml")
34
+ YAML.load(File.open(prefs)).each {|pref, value| eval("@#{pref} = #{value.inspect}")} if File.exists?(prefs)
35
+
36
+ @required = Array.new
37
+ @services = Array.new
38
+
39
+ # include all provided data
40
+ options.each do |method, value|
41
+ instance_variable_set("@#{method}", value)
42
+ end
43
+
44
+ case options[:carrier]
45
+ when "fedex"
46
+ fedex
47
+ when "ups"
48
+ ups
49
+ when nil
50
+ else
51
+ raise ShippingError, "unknown service"
52
+ end
53
+ end
54
+
55
+ # Initializes an instance of Shipping::FedEx with the same instance variables as the base object
56
+ def fedex
57
+ Shipping::FedEx.new prepare_vars
58
+ end
59
+
60
+ # Initializes an instance of Shipping::UPS with the same instance variables as the base object
61
+ def ups
62
+ Shipping::UPS.new prepare_vars
63
+ end
64
+
65
+ # Attempt to package items in multiple boxes efficiently
66
+ # This doesn't use the bin-packing algorithm, but instead attempts to mirror how people pack boxes
67
+ # -- since people will most likely be packing them.
68
+ # -- It attempts to pack like items whenever possible.
69
+ # @items: array of weights
70
+ # @weight_each: can be used instead of array of items
71
+ # @variation_threshold: how much variety you'll allow (default to 10% variation [e.g. 10 items, 10 each])
72
+ # @weight_threshold: the minimum weight a box must be to close (default .5, i.e. half full by weight)
73
+ # @quantity_threshold: the minimum full a box must be to close (default .5, i.e. half full by the number of items that will fit)
74
+ def boxes
75
+ # See if we're dealing with an array of items
76
+ if @items.length > 0
77
+ @items.each {|item| item[:total_weight] = item[:weight] * item[:quantity]} # get weight totals
78
+ props = @items.inject({:weights => [], :quantities => [], :total_weights => []}) {|h, item| h[:weights] << item[:weight];h[:quantities] << item[:quantity]; h[:total_weights] << item[:total_weight];h}
79
+ @quantity = props[:quantities].sum
80
+ total_weight = props[:total_weights].sum
81
+
82
+ # check to see if these are all the same weight
83
+ if props[:weights].uniq.length == 1
84
+ itemized = false
85
+ @weight_each = props[:weights].uniq[0]
86
+ else
87
+ itemized = true
88
+ end
89
+ else
90
+ @required = ['quantity', 'weight_each']
91
+ total_weight = @quantity.to_f * @weight_each
92
+ itemized = false
93
+ end
94
+
95
+ max_weight = @max_weight || 150 # Fed Ex and UPS commercial max
96
+ max_quantity = @max_quantity || @quantity
97
+ variation_threshold = @variation_threshold || 0.1 # default to 10% variation (e.g. 10 items, 10 each)
98
+ weight_threshold = @weight_threshold || 0.5 # default to half full
99
+ quantity_threshold = @quantity_threshold || 0.5 #default to half full
100
+ box = Array.new
101
+
102
+ # See if boxes should be divided by weight or number
103
+ bw = total_weight / max_weight
104
+ bq = @quantity.to_f / max_quantity
105
+ min_boxes = [bw.ceil, bq.ceil].max.to_i
106
+
107
+ # work with list of items
108
+ if itemized
109
+ leftovers = Array.new
110
+ variation = @items.length / @quantity.to_f # this shows us how much repetition there is
111
+
112
+ # First, we attempt to pack like items/weights
113
+ # we can skip this if variation is really high
114
+ if variation < variation_threshold
115
+ @items.each do |item|
116
+ while item[:quantity] > 0
117
+ max = (@max_weight / item[:weight]).truncate # how many of this weight can be packed in
118
+ this_num = [max, @max_quantity, item[:quantity]].min # should we pack by weight, number avail, or quantity
119
+ this_weight = this_num * item[:weight]
120
+ item[:quantity] -= this_num
121
+
122
+ # if we haven't met the threshold
123
+ if (this_weight / @max_weight) <= weight_threshold and (this_num / @max_quantity) <= quantity_threshold
124
+ leftovers << {:weight => this_weight, :quantity => this_num, :item => item[:id]}
125
+ else #otherwise, pack it
126
+ box << {:weight => this_weight, :quantity => this_num, :item => item[:id]}
127
+ end
128
+ end
129
+ end
130
+ else
131
+ leftovers = @items
132
+ end
133
+
134
+ # Then, we pack all the leftovers
135
+ leftover_box = {:weight => @max_weight, :quantity => @max_quantity}
136
+ this_box = {:weight => 0.0, :quantity => 0}
137
+ leftovers.each do |item|
138
+ for i in 1..item[:quantity]
139
+ leftover_box[:weight] -= item[:weight]
140
+ leftover_box[:quantity] -= 1
141
+ if leftover_box[:weight] > 0 and leftover_box[:quantity] > 0
142
+ this_box[:weight] += item[:weight]
143
+ this_box[:quantity] += 1
144
+ elsif leftover_box[:weight] = 0 and leftover_box[:quantity] >= 0
145
+ this_box[:weight] += item[:weight]
146
+ this_box[:quantity] += 1
147
+ box << {:weight => this_box[:weight], :quantity => this_box[:quantity]}
148
+ leftover_box = {:weight => @max_weight, :quantity => @max_quantity}
149
+ this_box = {:weight => 0.0, :quantity => 0}
150
+ else
151
+ box << {:weight => this_box[:weight], :quantity => this_box[:quantity]}
152
+ leftover_box = {:weight => @max_weight, :quantity => @max_quantity}
153
+ this_box = {:weight => 0.0, :quantity => 0}
154
+ end
155
+ end
156
+ end
157
+ if this_box[:weight] > 0.0 and this_box[:quantity] > 0
158
+ box << {:weight => this_box[:weight], :quantity => this_box[:quantity]}
159
+ end
160
+
161
+ inefficiency = box.length / min_boxes
162
+
163
+ else # pack super efficiently
164
+ if bw > bq
165
+ box_weight = max_weight
166
+ box_quantity = max_weight / @weight_each
167
+ else
168
+ box_weight = max_quantity * @weight_each
169
+ box_quantity = max_quantity
170
+ end
171
+
172
+ # fill the rest of the boxes
173
+ num_boxes = min_boxes - 1
174
+ (num_boxes).times do
175
+ box << {:weight => box_weight, :quantity => box_quantity}
176
+ end
177
+
178
+ # if there is an uneven number for packaging
179
+ if @quantity % min_boxes != 0 or num_boxes == 0
180
+ excess_q = @quantity - (box_quantity * num_boxes)
181
+ excess_w = excess_q * @weight_each
182
+ box << {:weight => excess_w, :quantity => excess_q}
183
+ end
184
+ inefficiency = 0
185
+ end
186
+ return box
187
+ end
188
+
189
+ def self.state_from_zip(zip)
190
+ zip = zip.to_i
191
+ {
192
+ (99500...99929) => "AK",
193
+ (35000...36999) => "AL",
194
+ (71600...72999) => "AR",
195
+ (75502...75505) => "AR",
196
+ (85000...86599) => "AZ",
197
+ (90000...96199) => "CA",
198
+ (80000...81699) => "CO",
199
+ (6000...6999) => "CT",
200
+ (20000...20099) => "DC",
201
+ (20200...20599) => "DC",
202
+ (19700...19999) => "DE",
203
+ (32000...33999) => "FL",
204
+ (34100...34999) => "FL",
205
+ (30000...31999) => "GA",
206
+ (96700...96798) => "HI",
207
+ (96800...96899) => "HI",
208
+ (50000...52999) => "IA",
209
+ (83200...83899) => "ID",
210
+ (60000...62999) => "IL",
211
+ (46000...47999) => "IN",
212
+ (66000...67999) => "KS",
213
+ (40000...42799) => "KY",
214
+ (45275...45275) => "KY",
215
+ (70000...71499) => "LA",
216
+ (71749...71749) => "LA",
217
+ (1000...2799) => "MA",
218
+ (20331...20331) => "MD",
219
+ (20600...21999) => "MD",
220
+ (3801...3801) => "ME",
221
+ (3804...3804) => "ME",
222
+ (3900...4999) => "ME",
223
+ (48000...49999) => "MI",
224
+ (55000...56799) => "MN",
225
+ (63000...65899) => "MO",
226
+ (38600...39799) => "MS",
227
+ (59000...59999) => "MT",
228
+ (27000...28999) => "NC",
229
+ (58000...58899) => "ND",
230
+ (68000...69399) => "NE",
231
+ (3000...3803) => "NH",
232
+ (3809...3899) => "NH",
233
+ (7000...8999) => "NJ",
234
+ (87000...88499) => "NM",
235
+ (89000...89899) => "NV",
236
+ (400...599) => "NY",
237
+ (6390...6390) => "NY",
238
+ (9000...14999) => "NY",
239
+ (43000...45999) => "OH",
240
+ (73000...73199) => "OK",
241
+ (73400...74999) => "OK",
242
+ (97000...97999) => "OR",
243
+ (15000...19699) => "PA",
244
+ (2800...2999) => "RI",
245
+ (6379...6379) => "RI",
246
+ (29000...29999) => "SC",
247
+ (57000...57799) => "SD",
248
+ (37000...38599) => "TN",
249
+ (72395...72395) => "TN",
250
+ (73300...73399) => "TX",
251
+ (73949...73949) => "TX",
252
+ (75000...79999) => "TX",
253
+ (88501...88599) => "TX",
254
+ (84000...84799) => "UT",
255
+ (20105...20199) => "VA",
256
+ (20301...20301) => "VA",
257
+ (20370...20370) => "VA",
258
+ (22000...24699) => "VA",
259
+ (5000...5999) => "VT",
260
+ (98000...99499) => "WA",
261
+ (49936...49936) => "WI",
262
+ (53000...54999) => "WI",
263
+ (24700...26899) => "WV",
264
+ (82000...83199) => "WY"
265
+ }.each do |range, state|
266
+ return state if range.include? zip
267
+ end
268
+
269
+ raise ShippingError, "Invalid zip code"
270
+ end
271
+
272
+ private
273
+
274
+ def prepare_vars #:nodoc:
275
+ h = eval(%q{instance_variables.map {|var| "#{var.gsub("@",":")} => #{eval(var+'.inspect')}"}.join(", ").chomp(", ")})
276
+ return eval("{#{h}}")
277
+ end
278
+
279
+ # Goes out, posts the data, and sets the @response variable with the information
280
+ def get_response(url)
281
+ check_required
282
+ uri = URI.parse url
283
+ http = Net::HTTP.new uri.host, uri.port
284
+ if uri.port == 443
285
+ http.use_ssl = true
286
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
287
+ end
288
+ @response_plain = http.post(uri.path, @data).body
289
+ @response = @response_plain.include?('<?xml') ? REXML::Document.new(@response_plain) : @response_plain
290
+
291
+ @response.instance_variable_set "@response_plain", @response_plain
292
+ def @response.plain; @response_plain; end
293
+ end
294
+
295
+ # Make sure that the required fields are not empty
296
+ def check_required
297
+ for var in @required
298
+ raise ShippingRequiredFieldError, "The #{var} variable needs to be set" if eval("@#{var}").nil?
299
+ end
300
+ end
301
+
302
+ STATES = {"al" => "alabama", "ne" => "nebraska", "ak" => "alaska", "nv" => "nevada", "az" => "arizona", "nh" => "new hampshire", "ar" => "arkansas", "nj" => "new jersey", "ca" => "california", "nm" => "new mexico", "co" => "colorado", "ny" => "new york", "ct" => "connecticut", "nc" => "north carolina", "de" => "delaware", "nd" => "north dakota", "fl" => "florida", "oh" => "ohio", "ga" => "georgia", "ok" => "oklahoma", "hi" => "hawaii", "or" => "oregon", "id" => "idaho", "pa" => "pennsylvania", "il" => "illinois", "pr" => "puerto rico", "in" => "indiana", "ri" => "rhode island", "ia" => "iowa", "sc" => "south carolina", "ks" => "kansas", "sd" => "south dakota", "ky" => "kentucky", "tn" => "tennessee", "la" => "louisiana", "tx" => "texas", "me" => "maine", "ut" => "utah", "md" => "maryland", "vt" => "vermont", "ma" => "massachusetts", "va" => "virginia", "mi" => "michigan", "wa" => "washington", "mn" => "minnesota", "dc" => "district of columbia", "ms" => "mississippi", "wv" => "west virginia", "mo" => "missouri", "wi" => "wisconsin", "mt" => "montana", "wy" => "wyoming"}
303
+
304
+ def self.initialize_for_fedex_service(xml)
305
+ s = Shipping::Base.new
306
+ s.fedex
307
+ s.eta = REXML::XPath.first(xml, "DeliveryDate").text unless REXML::XPath.match(xml, "DeliveryDate").empty?
308
+ s.service_type = REXML::XPath.first(xml, "Service").text
309
+ s.discount_price = REXML::XPath.first(xml, "EstimatedCharges/DiscountedCharges/BaseCharge").text
310
+ s.price = REXML::XPath.first(xml, "EstimatedCharges/DiscountedCharges/NetCharge").text
311
+ return s
312
+ end
313
+ end
314
+ end