spree_active_shipping 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/README.md +67 -0
- data/Rakefile +45 -0
- data/app/controllers/admin/shipments_controller_decorator.rb +11 -0
- data/app/controllers/admin/shipping_methods_controller_decorator.rb +15 -0
- data/app/controllers/checkout_controller_decorator.rb +11 -0
- data/app/models/calculator/active_shipping.rb +126 -0
- data/app/models/calculator/fedex/base.rb +8 -0
- data/app/models/calculator/fedex/base.rb~ +7 -0
- data/app/models/calculator/fedex/express_saver.rb +5 -0
- data/app/models/calculator/fedex/first_overnight.rb +5 -0
- data/app/models/calculator/fedex/ground.rb +5 -0
- data/app/models/calculator/fedex/ground_home_delivery.rb +5 -0
- data/app/models/calculator/fedex/international_economy.rb +5 -0
- data/app/models/calculator/fedex/international_economy_freight.rb +5 -0
- data/app/models/calculator/fedex/international_first.rb +5 -0
- data/app/models/calculator/fedex/international_ground.rb +5 -0
- data/app/models/calculator/fedex/international_priority.rb +5 -0
- data/app/models/calculator/fedex/international_priority_freight.rb +5 -0
- data/app/models/calculator/fedex/international_priority_saturday_delivery.rb +5 -0
- data/app/models/calculator/fedex/one_day_freight.rb +5 -0
- data/app/models/calculator/fedex/one_day_freight_saturday_delivery.rb +5 -0
- data/app/models/calculator/fedex/priority_overnight.rb +5 -0
- data/app/models/calculator/fedex/priority_overnight_saturday_delivery.rb +5 -0
- data/app/models/calculator/fedex/saver.rb +5 -0
- data/app/models/calculator/fedex/saver.rb~ +5 -0
- data/app/models/calculator/fedex/standard_overnight.rb +5 -0
- data/app/models/calculator/fedex/three_day_freight.rb +5 -0
- data/app/models/calculator/fedex/three_day_freight_saturday_delivery.rb +5 -0
- data/app/models/calculator/fedex/two_day.rb +5 -0
- data/app/models/calculator/fedex/two_day_freight.rb +5 -0
- data/app/models/calculator/fedex/two_day_freight_saturday_delivery.rb +5 -0
- data/app/models/calculator/fedex/two_day_saturday_delivery.rb +5 -0
- data/app/models/calculator/ups/base.rb +15 -0
- data/app/models/calculator/ups/ground.rb +5 -0
- data/app/models/calculator/ups/next_day_air.rb +5 -0
- data/app/models/calculator/ups/next_day_air_early_am.rb +5 -0
- data/app/models/calculator/ups/next_day_air_saver.rb +5 -0
- data/app/models/calculator/ups/saver.rb +5 -0
- data/app/models/calculator/ups/second_day_air.rb +5 -0
- data/app/models/calculator/ups/three_day_select.rb +5 -0
- data/app/models/calculator/ups/worldwide_expedited.rb +5 -0
- data/app/models/calculator/usps/base.rb +5 -0
- data/app/models/calculator/usps/express_mail.rb +5 -0
- data/app/models/calculator/usps/express_mail_international.rb +5 -0
- data/app/models/calculator/usps/media_mail.rb +5 -0
- data/app/models/calculator/usps/priority_mail.rb +5 -0
- data/app/models/calculator/usps/priority_mail_international.rb +5 -0
- data/app/models/calculator/usps/priority_mail_large_flat_rate_box.rb +5 -0
- data/app/models/calculator/usps/priority_mail_regular_medium_flat_rate_boxes.rb +5 -0
- data/app/models/calculator/usps/priority_mail_small_flat_rate_box.rb +5 -0
- data/app/views/admin/shipping_methods/_form.html.erb +42 -0
- data/config/locales/en.yml +3 -0
- data/lib/active_shipping_configuration.rb +25 -0
- data/lib/spree/active_shipping/config.rb +22 -0
- data/lib/spree/active_shipping/ups_override.rb +274 -0
- data/lib/spree/shipping_error.rb +3 -0
- data/lib/spree_active_shipping.rb +74 -0
- data/lib/tasks/active_shipping_extension_tasks.rake +29 -0
- data/spec/lib/spree/bogus_calculator.rb +11 -0
- data/spec/lib/spree/bogus_carrier.rb +15 -0
- data/spec/models/active_shipping_calculator_spec.rb +51 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +33 -0
- data/spree_active_shipping.gemspec +23 -0
- metadata +184 -0
data/.gitignore
ADDED
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|