spree_active_shipping 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.gitignore +2 -0
  2. data/README.md +67 -0
  3. data/Rakefile +45 -0
  4. data/app/controllers/admin/shipments_controller_decorator.rb +11 -0
  5. data/app/controllers/admin/shipping_methods_controller_decorator.rb +15 -0
  6. data/app/controllers/checkout_controller_decorator.rb +11 -0
  7. data/app/models/calculator/active_shipping.rb +126 -0
  8. data/app/models/calculator/fedex/base.rb +8 -0
  9. data/app/models/calculator/fedex/base.rb~ +7 -0
  10. data/app/models/calculator/fedex/express_saver.rb +5 -0
  11. data/app/models/calculator/fedex/first_overnight.rb +5 -0
  12. data/app/models/calculator/fedex/ground.rb +5 -0
  13. data/app/models/calculator/fedex/ground_home_delivery.rb +5 -0
  14. data/app/models/calculator/fedex/international_economy.rb +5 -0
  15. data/app/models/calculator/fedex/international_economy_freight.rb +5 -0
  16. data/app/models/calculator/fedex/international_first.rb +5 -0
  17. data/app/models/calculator/fedex/international_ground.rb +5 -0
  18. data/app/models/calculator/fedex/international_priority.rb +5 -0
  19. data/app/models/calculator/fedex/international_priority_freight.rb +5 -0
  20. data/app/models/calculator/fedex/international_priority_saturday_delivery.rb +5 -0
  21. data/app/models/calculator/fedex/one_day_freight.rb +5 -0
  22. data/app/models/calculator/fedex/one_day_freight_saturday_delivery.rb +5 -0
  23. data/app/models/calculator/fedex/priority_overnight.rb +5 -0
  24. data/app/models/calculator/fedex/priority_overnight_saturday_delivery.rb +5 -0
  25. data/app/models/calculator/fedex/saver.rb +5 -0
  26. data/app/models/calculator/fedex/saver.rb~ +5 -0
  27. data/app/models/calculator/fedex/standard_overnight.rb +5 -0
  28. data/app/models/calculator/fedex/three_day_freight.rb +5 -0
  29. data/app/models/calculator/fedex/three_day_freight_saturday_delivery.rb +5 -0
  30. data/app/models/calculator/fedex/two_day.rb +5 -0
  31. data/app/models/calculator/fedex/two_day_freight.rb +5 -0
  32. data/app/models/calculator/fedex/two_day_freight_saturday_delivery.rb +5 -0
  33. data/app/models/calculator/fedex/two_day_saturday_delivery.rb +5 -0
  34. data/app/models/calculator/ups/base.rb +15 -0
  35. data/app/models/calculator/ups/ground.rb +5 -0
  36. data/app/models/calculator/ups/next_day_air.rb +5 -0
  37. data/app/models/calculator/ups/next_day_air_early_am.rb +5 -0
  38. data/app/models/calculator/ups/next_day_air_saver.rb +5 -0
  39. data/app/models/calculator/ups/saver.rb +5 -0
  40. data/app/models/calculator/ups/second_day_air.rb +5 -0
  41. data/app/models/calculator/ups/three_day_select.rb +5 -0
  42. data/app/models/calculator/ups/worldwide_expedited.rb +5 -0
  43. data/app/models/calculator/usps/base.rb +5 -0
  44. data/app/models/calculator/usps/express_mail.rb +5 -0
  45. data/app/models/calculator/usps/express_mail_international.rb +5 -0
  46. data/app/models/calculator/usps/media_mail.rb +5 -0
  47. data/app/models/calculator/usps/priority_mail.rb +5 -0
  48. data/app/models/calculator/usps/priority_mail_international.rb +5 -0
  49. data/app/models/calculator/usps/priority_mail_large_flat_rate_box.rb +5 -0
  50. data/app/models/calculator/usps/priority_mail_regular_medium_flat_rate_boxes.rb +5 -0
  51. data/app/models/calculator/usps/priority_mail_small_flat_rate_box.rb +5 -0
  52. data/app/views/admin/shipping_methods/_form.html.erb +42 -0
  53. data/config/locales/en.yml +3 -0
  54. data/lib/active_shipping_configuration.rb +25 -0
  55. data/lib/spree/active_shipping/config.rb +22 -0
  56. data/lib/spree/active_shipping/ups_override.rb +274 -0
  57. data/lib/spree/shipping_error.rb +3 -0
  58. data/lib/spree_active_shipping.rb +74 -0
  59. data/lib/tasks/active_shipping_extension_tasks.rake +29 -0
  60. data/spec/lib/spree/bogus_calculator.rb +11 -0
  61. data/spec/lib/spree/bogus_carrier.rb +15 -0
  62. data/spec/models/active_shipping_calculator_spec.rb +51 -0
  63. data/spec/spec.opts +6 -0
  64. data/spec/spec_helper.rb +33 -0
  65. data/spree_active_shipping.gemspec +23 -0
  66. metadata +184 -0
@@ -0,0 +1,2 @@
1
+ spec/test_app
2
+ *.swp
@@ -0,0 +1,67 @@
1
+ Active Shipping
2
+ ===============
3
+
4
+ This is a Spree extension that wraps the popular [active_shipping](http://github.com/Shopify/active_shipping/tree/master) plugin.
5
+
6
+ UPS
7
+ ---
8
+
9
+ You will need a UPS developer account to get rate quotes (even in development and test mode.) The UPS calculators also require that you set the following configuration properties. You will also need a USPS account to use USPS rate quote system. Note: acquiring the USPS developer account is easy but has a confusion activation step. Look for related sicussion in ActiveShipping's original discussion group as well as carefully read your USPS confirmation email.
10
+
11
+ <pre>
12
+ Spree::ActiveShipping::Config[:origin_country]
13
+ Spree::ActiveShipping::Config[:origin_city]
14
+ Spree::ActiveShipping::Config[:origin_state]
15
+ Spree::ActiveShipping::Config[:origin_zip]
16
+ Spree::ActiveShipping::Config[:ups_login]
17
+ Spree::ActiveShipping::Config[:ups_password]
18
+ Spree::ActiveShipping::Config[:ups_key]
19
+ Spree::ActiveShipping::Config[:usps_login]
20
+ </pre>
21
+
22
+ It will soon be possible to set these properties through a new admin configuration screen (even sooner if someone else writes the patch!)
23
+
24
+ Global Handling Fee
25
+ -------------------
26
+
27
+ <pre>
28
+ Spree::ActiveShipping::Config[:handling_fee]
29
+ </pre>
30
+
31
+ This property allows you to set a global handling fee that will be added to all calculated shipping rates. Specify the number of cents, not dollars.
32
+
33
+ Using Migrations
34
+ ----------------
35
+
36
+ If you'd like to set your shipping configuration as part of a migration you could add something like this to your site extension.
37
+
38
+ <pre>
39
+ class AddUpsConfiguration < ActiveRecord::Migration
40
+ def self.up
41
+ Spree::ActiveShipping::Config.set(:ups_login => "dpbrowning")
42
+ Spree::ActiveShipping::Config.set(:ups_password => "doublewide")
43
+ Spree::ActiveShipping::Config.set(:ups_key => "5B8DDE509EFDA5D6")
44
+ Spree::ActiveShipping::Config.set(:usps_login => "13658997AOE2568XOE")
45
+ end
46
+
47
+ def self.down
48
+ end
49
+ end
50
+ </pre>
51
+
52
+ Installation
53
+ ------------
54
+
55
+ 1. Add the following to your applications Gemfile
56
+
57
+ gem 'spree_active_shipping'
58
+
59
+ 2. Run bundler
60
+
61
+ bundle install
62
+
63
+
64
+ Further Reading
65
+ ---------------
66
+
67
+ Andrea Singh has also written an excellent [blog post](http://blog.madebydna.com/all/code/2010/05/26/setting-up-usps-shipping-with-spree.html) covering the use of this extension in detail.
@@ -0,0 +1,45 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new
6
+
7
+ require 'cucumber/rake/task'
8
+ Cucumber::Rake::Task.new do |t|
9
+ t.cucumber_opts = %w{--format pretty}
10
+ end
11
+
12
+ desc "Regenerates a rails 3 app for testing"
13
+ task :test_app do
14
+ SPREE_PATH = ENV['SPREE_PATH']
15
+ raise "SPREE_PATH should be specified" unless SPREE_PATH
16
+ require File.join(SPREE_PATH, 'lib/generators/spree/test_app_generator')
17
+ class SpreeActiveShippingTestAppGenerator < Spree::Generators::TestAppGenerator
18
+ def tweak_gemfile
19
+ append_file 'Gemfile' do
20
+ <<-gems
21
+ gem 'spree_core', :path => '#{File.join(SPREE_PATH, 'core')}'
22
+ gem 'spree_active_shipping', :path => '#{File.dirname(__FILE__)}'
23
+ gems
24
+ end
25
+ end
26
+
27
+ def install_gems
28
+ inside "test_app" do
29
+ run 'rake spree_core:install'
30
+ end
31
+ end
32
+
33
+ def migrate_db
34
+ run_migrations
35
+ end
36
+ end
37
+ SpreeActiveShippingTestAppGenerator.start
38
+ end
39
+
40
+ namespace :test_app do
41
+ desc 'Rebuild test and cucumber databases'
42
+ task :rebuild_dbs do
43
+ system("cd spec/test_app && rake db:drop db:migrate RAILS_ENV=test && rake db:drop db:migrate RAILS_ENV=cucumber")
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ # handle shipping errors gracefully on admin ui
2
+ Admin::ShipmentsController.class_eval do
3
+ rescue_from Spree::ShippingError, :with => :handle_shipping_error
4
+
5
+ private
6
+ def handle_shipping_error(e)
7
+ load_object
8
+ flash.now[:error] = e.message
9
+ render :action => "edit"
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ Admin::ShippingMethodsController.class_eval do
2
+
3
+ before_filter :load_data
4
+
5
+ private
6
+
7
+ # overriding to return an array of Calculator objects instead of a Set
8
+ def load_data
9
+ @available_zones = Zone.find :all, :order => :name
10
+ @calculators = []
11
+ ShippingMethod.calculators.each {|calc|
12
+ @calculators << eval(calc.name).new
13
+ }
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # handle shipping errors gracefully during checkout
2
+ CheckoutController.class_eval do
3
+
4
+ rescue_from Spree::ShippingError, :with => :handle_shipping_error
5
+
6
+ private
7
+ def handle_shipping_error(e)
8
+ flash[:error] = e.message
9
+ redirect_to checkout_state_path(:address)
10
+ end
11
+ end
@@ -0,0 +1,126 @@
1
+ # This is a base calculator for shipping calcualations using the ActiveShipping plugin. It is not intended to be
2
+ # instantiated directly. Create sublcass for each specific shipping method you wish to support instead.
3
+ class Calculator::ActiveShipping < Calculator
4
+
5
+ include ActiveMerchant::Shipping
6
+
7
+ def self.register
8
+ super
9
+ ShippingMethod.register_calculator(self)
10
+ end
11
+
12
+ def compute(object)
13
+ if object.is_a?(Array)
14
+ order = object.first.order
15
+ elsif object.is_a?(Shipment)
16
+ order = object.order
17
+ else
18
+ order = object
19
+ end
20
+ origin= Location.new(:country => Spree::ActiveShipping::Config[:origin_country],
21
+ :city => Spree::ActiveShipping::Config[:origin_city],
22
+ :state => Spree::ActiveShipping::Config[:origin_state],
23
+ :zip => Spree::ActiveShipping::Config[:origin_zip])
24
+
25
+ addr = order.ship_address
26
+
27
+ destination = Location.new(:country => addr.country.iso,
28
+ :state => (addr.state ? addr.state.abbr : addr.state_name),
29
+ :city => addr.city,
30
+ :zip => addr.zipcode)
31
+
32
+ rates = Rails.cache.fetch(cache_key(order)) do
33
+ rates = retrieve_rates(origin, destination, packages(order))
34
+ end
35
+
36
+ return nil if rates.empty?
37
+ rate = rates[self.class.description].to_f + (Spree::ActiveShipping::Config[:handling_fee].to_f || 0.0)
38
+ return nil unless rate
39
+ # divide by 100 since active_shipping rates are expressed as cents
40
+ return rate/100.0
41
+ end
42
+
43
+
44
+ def timing(line_items)
45
+ order = line_items.first.order
46
+ origin = Location.new(:country => Spree::ActiveShipping::Config[:origin_country],
47
+ :city => Spree::ActiveShipping::Config[:origin_city],
48
+ :state => Spree::ActiveShipping::Config[:origin_state],
49
+ :zip => Spree::ActiveShipping::Config[:origin_zip])
50
+ addr = order.ship_address
51
+ destination = Location.new(:country => addr.country.iso,
52
+ :state => (addr.state ? addr.state.abbr : addr.state_name),
53
+ :city => addr.city,
54
+ :zip => addr.zipcode)
55
+ timings = Rails.cache.fetch(cache_key(line_items)+"-timings") do
56
+ timings = retrieve_timings(origin, destination, packages(order))
57
+ end
58
+ return nil if timings.nil? || !timings.is_a?(Hash) || timings.empty?
59
+ return timings[self.description]
60
+
61
+ end
62
+
63
+ private
64
+ def retrieve_rates(origin, destination, packages)
65
+ begin
66
+ response = carrier.find_rates(origin, destination, packages)
67
+ # turn this beastly array into a nice little hash
68
+ rate_hash = Hash[*response.rates.collect { |rate| [rate.service_name, rate.price] }.flatten]
69
+ return rate_hash
70
+ rescue ActiveMerchant::ActiveMerchantError => e
71
+
72
+ if [ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError].include? e.class
73
+ params = e.response.params
74
+ if params.has_key?("Response") && params["Response"].has_key?("Error") && params["Response"]["Error"].has_key?("ErrorDescription")
75
+ message = params["Response"]["Error"]["ErrorDescription"]
76
+ else
77
+ message = e.message
78
+ end
79
+ else
80
+ message = e.to_s
81
+ end
82
+
83
+ Rails.cache.write @cache_key, {} #write empty hash to cache to prevent constant re-lookups
84
+
85
+ raise Spree::ShippingError.new("#{I18n.t('shipping_error')}: #{message}")
86
+ end
87
+
88
+ end
89
+
90
+
91
+ def retrieve_timings(origin, destination, packages)
92
+ begin
93
+ if carrier.respond_to?(:find_time_in_transit)
94
+ response = carrier.find_time_in_transit(origin, destination, packages)
95
+ return response
96
+ end
97
+ rescue ActiveMerchant::Shipping::ResponseError => re
98
+ params = re.response.params
99
+ if params.has_key?("Response") && params["Response"].has_key?("Error") && params["Response"]["Error"].has_key?("ErrorDescription")
100
+ message = params["Response"]["Error"]["ErrorDescription"]
101
+ elsee
102
+ message = re.message
103
+ end
104
+ Rails.cache.write @cache_key+'-', {} #write empty hash to cache to prevent constant re-lookups
105
+ raise Spree::ShippingError.new("#{I18n.t('shipping_error')}: #{message}")
106
+ end
107
+ end
108
+
109
+
110
+ private
111
+
112
+ # Generates an array of Package objects based on the quantities and weights of the variants in the line items
113
+ def packages(order)
114
+ multiplier = Spree::ActiveShipping::Config[:unit_multiplier]
115
+ weight = order.line_items.inject(0) do |weight, line_item|
116
+ weight + (line_item.variant.weight ? (line_item.quantity * line_item.variant.weight * multiplier) : 0)
117
+ end
118
+ package = Package.new(weight, [], :units => Spree::ActiveShipping::Config[:units].to_sym)
119
+ [package]
120
+ end
121
+
122
+ def cache_key(order)
123
+ addr = order.ship_address
124
+ @cache_key = "#{carrier.name}-#{order.number}-#{addr.country.iso}-#{addr.state ? addr.state.abbr : addr.state_name}-#{addr.city}-#{addr.zipcode}-#{order.line_items.map {|li| li.variant_id.to_s + "_" + li.quantity.to_s }.join("|")}".gsub(" ","")
125
+ end
126
+ end
@@ -0,0 +1,8 @@
1
+ class Calculator::Fedex::Base < Calculator::ActiveShipping
2
+ def carrier
3
+ ActiveMerchant::Shipping::FedEx.new(:key => Spree::ActiveShipping::Config[:fedex_key],
4
+ :password => Spree::ActiveShipping::Config[:fedex_password],
5
+ :account => Spree::ActiveShipping::Config[:fedex_account],
6
+ :login => Spree::ActiveShipping::Config[:fedex_login])
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ class Calculator::Fedex::Base < Calculator::ActiveShipping
2
+ def carrier
3
+ ActiveMerchant::Shipping::FEDEX.new(:login => Spree::ActiveShipping::Config[:fedex_login],
4
+ :password => Spree::ActiveShipping::Config[:fedex_password],
5
+ :key => Spree::ActiveShipping::Config[:fedex_key])
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::ExpressSaver < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx Express Saver"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::FirstOvernight < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx First Overnight"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::Ground < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx Ground"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::GroundHomeDelivery < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx Ground Home Delivery"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::InternationalEconomy < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx International Economy"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::InternationalEconomyFreight < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx International Economy Freight"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::InternationalFirst < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx International First"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::InternationalGround < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx International Ground"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::InternationalPriority < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx International Priority"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::InternationalPriorityFreight < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx International Priority Freight"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::InternationalPrioritySaturdayDelivery < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx International Priority Saturday Delivery"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::OneDayFreight < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx 1 Day Freight"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::OneDayFreightSaturdayDelivery < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx 1 Day Freight Saturday Delivery"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::PriorityOvernight < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx Priority Overnight"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::PriorityOvernightSaturdayDelivery < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx Priority Overnight Saturday Delivery"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::Saver < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx Saver"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Ups::Saver < Calculator::Ups::Base
2
+ def self.description
3
+ "UPS Saver"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::StandardOvernight < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx Standard Overnight"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::ThreeDayFreight < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx 3 Day Freight"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::ThreeDayFreightSaturdayDelivery < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx 3 Day Freight Saturday Delivery"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Calculator::Fedex::TwoDay < Calculator::Fedex::Base
2
+ def self.description
3
+ "FedEx 2 Day"
4
+ end
5
+ end