carter 0.5.5
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.
- data/LICENSE.txt +20 -0
- data/README.md +26 -0
- data/app/controllers/cart_items_controller.rb +34 -0
- data/app/controllers/carts_controller.rb +11 -0
- data/app/models/cart.rb +34 -0
- data/app/models/cart_item.rb +40 -0
- data/app/views/carts/show.html.erb +0 -0
- data/generators/carter_generator.rb +21 -0
- data/generators/carter_install_generator.rb +20 -0
- data/generators/templates/migration.rb +37 -0
- data/init.rb +1 -0
- data/lib/carter.rb +8 -0
- data/lib/carter/active_record/extensions.rb +22 -0
- data/lib/carter/cart.rb +66 -0
- data/lib/carter/cartable.rb +60 -0
- data/lib/carter/controller_additions.rb +86 -0
- data/lib/carter/controller_resource.rb +79 -0
- data/lib/carter/engine.rb +8 -0
- data/lib/carter/errors.rb +2 -0
- data/lib/carter/errors/multiple_cart_items_not_allowed.rb +5 -0
- data/lib/carter/errors/setup_error.rb +5 -0
- data/lib/carter/initializer.rb +61 -0
- data/lib/carter/routing.rb +11 -0
- data/lib/carter/state_machine.rb +81 -0
- data/lib/tasks/carter.rake +13 -0
- data/rails/init.rb +7 -0
- data/spec/active_record/cart_item_spec.rb +97 -0
- data/spec/active_record/cart_spec.rb +103 -0
- data/spec/active_record/cartable_spec.rb +116 -0
- data/spec/app/controllers/cart_items_controller_spec.rb +19 -0
- data/spec/carter_spec.rb +5 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/state_machine_spec.rb +100 -0
- data/spec/support/active_record_helper.rb +79 -0
- data/spec/support/factories.rb +25 -0
- metadata +265 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
module Carter
|
2
|
+
# Handle the load cart controller logic so we don't clutter up all controllers with non-interface methods.
|
3
|
+
# This class is used internally, so you do not need to call methods directly on it.
|
4
|
+
|
5
|
+
class ControllerResource # :nodoc:
|
6
|
+
def self.add_before_filter(controller_class, method, *args)
|
7
|
+
options = args.extract_options!
|
8
|
+
resource_name = args.first
|
9
|
+
controller_class.before_filter(options.slice(:only, :except)) do |controller|
|
10
|
+
ControllerResource.new(controller, resource_name, options.except(:only, :except)).send(method)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(controller, *args)
|
15
|
+
@controller = controller
|
16
|
+
@params = controller.params
|
17
|
+
@session = controller.session
|
18
|
+
@options = args.extract_options!
|
19
|
+
@name = args.first
|
20
|
+
end
|
21
|
+
|
22
|
+
def load_cart
|
23
|
+
store_shopping_location if @controller.shopping? && !@controller.checking_out?
|
24
|
+
@controller.instance_variable_set("@cart", load_cart_instance)
|
25
|
+
@controller.instance_variable_set("@shopper", current_shopper)
|
26
|
+
end
|
27
|
+
|
28
|
+
def load_cart_for_checkout
|
29
|
+
@controller.instance_variable_set("@cart", load_cart_instance)
|
30
|
+
@controller.instance_variable_set("@shopper", current_shopper)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def load_cart_instance
|
36
|
+
find_cart_by_session || find_cart_by_shopper || ::Cart.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def member_action?
|
40
|
+
!collection_actions.include? @params[:action].to_sym
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_cart_by_shopper
|
44
|
+
::Cart.with_state(default_cart_state).first(:conditions => {:shopper_id => current_shopper.id, :shopper_type => current_shopper.class.name} ) if current_shopper && Carter.settings.persist_by_shopper?
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_cart_by_session
|
48
|
+
::Cart.with_state(default_cart_state).find_by_id(@session[:cart_id])
|
49
|
+
end
|
50
|
+
|
51
|
+
def current_shopper
|
52
|
+
@controller.respond_to?(:current_user) ? @controller.send(:current_user) : nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def store_shopping_location
|
56
|
+
@session[:continue_shopping_url] = @controller.request.request_uri
|
57
|
+
end
|
58
|
+
|
59
|
+
def name
|
60
|
+
@name || name_from_controller
|
61
|
+
end
|
62
|
+
|
63
|
+
def name_from_controller
|
64
|
+
@params[:controller].sub("Controller", "").underscore.split('/').last.singularize
|
65
|
+
end
|
66
|
+
|
67
|
+
def collection_actions
|
68
|
+
[:index] + [@options[:collection]].flatten
|
69
|
+
end
|
70
|
+
|
71
|
+
def new_actions
|
72
|
+
[:new, :create] + [@options[:new]].flatten
|
73
|
+
end
|
74
|
+
|
75
|
+
def default_cart_state
|
76
|
+
@options[:state] || :active
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Carter
|
2
|
+
class << self
|
3
|
+
# The Settings instance used to configure the Carter environment
|
4
|
+
def settings
|
5
|
+
@@settings
|
6
|
+
rescue
|
7
|
+
raise SetupError, "You must create the Carter settings using the Carter::Initializer.setup method"
|
8
|
+
end
|
9
|
+
|
10
|
+
def settings=(settings)
|
11
|
+
@@settings = settings
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
class Initializer
|
17
|
+
attr_reader :settings
|
18
|
+
|
19
|
+
def initialize(settings)
|
20
|
+
@settings = settings
|
21
|
+
end
|
22
|
+
|
23
|
+
# setup any defaults in here.
|
24
|
+
def process
|
25
|
+
Carter.settings = settings
|
26
|
+
end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
|
30
|
+
# This is useful if you only want the load path initialized, without
|
31
|
+
# incurring the overhead of completely loading the entire environment.
|
32
|
+
def setup(command = :process, settings = Settings.new)
|
33
|
+
yield settings if block_given?
|
34
|
+
initializer = new settings
|
35
|
+
initializer.send(command)
|
36
|
+
initializer
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Settings
|
43
|
+
attr_accessor :persist_by_shopper
|
44
|
+
attr_accessor :on_checkout
|
45
|
+
attr_accessor :on_failed
|
46
|
+
attr_accessor :cartables
|
47
|
+
attr_accessor :default_currency
|
48
|
+
attr_accessor :payment_method
|
49
|
+
|
50
|
+
def persist_by_shopper?
|
51
|
+
@persist_by_shopper
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
self.cartables = []
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'state_machine'
|
2
|
+
module Carter
|
3
|
+
module StateMachine
|
4
|
+
module Cart
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
base.init_states
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def init_states
|
12
|
+
|
13
|
+
state_machine :state, :initial => :active do
|
14
|
+
state :active, :processing, :failure, :success, :expired
|
15
|
+
|
16
|
+
event :checkout do
|
17
|
+
transition [:active, :failure] => :processing
|
18
|
+
end
|
19
|
+
|
20
|
+
event :succeeded do
|
21
|
+
transition :processing => :success
|
22
|
+
end
|
23
|
+
|
24
|
+
event :failed do
|
25
|
+
transition :processing => :failure
|
26
|
+
end
|
27
|
+
|
28
|
+
event :expire do
|
29
|
+
transition :active => :expired
|
30
|
+
end
|
31
|
+
|
32
|
+
after_transition :on => :succeeded, :do => :on_success # gateway says yes - to what is needed to add the products to the buyer.
|
33
|
+
|
34
|
+
# +checkout hook+ - probably goes off to payment gateway.
|
35
|
+
# expects another transition to be made in here to continue the cart lifecycle
|
36
|
+
# possible actions are `cart.failed`, `cart.succeeded` for now that should do it.
|
37
|
+
after_transition :on => :checkout, :do => :on_checkout
|
38
|
+
after_transition :on => :failed, :do => :on_failed
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module CartItem
|
45
|
+
def self.included(base)
|
46
|
+
base.extend ClassMethods
|
47
|
+
base.send(:include, InstanceMethods)
|
48
|
+
base.init_states
|
49
|
+
end
|
50
|
+
|
51
|
+
module ClassMethods
|
52
|
+
def init_states
|
53
|
+
|
54
|
+
state_machine :state, :initial => :in_cart do
|
55
|
+
state :in_cart, :processing, :failure, :purchased
|
56
|
+
event :add_to_owner do
|
57
|
+
transition [:in_cart] => :processing
|
58
|
+
end
|
59
|
+
|
60
|
+
event :succeeded do
|
61
|
+
transition :processing => :purchased
|
62
|
+
end
|
63
|
+
|
64
|
+
event :failed do
|
65
|
+
transition :processing => :failure
|
66
|
+
end
|
67
|
+
|
68
|
+
after_transition :on => :add_to_owner, :do => :on_purchase
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
module InstanceMethods
|
74
|
+
def on_purchase
|
75
|
+
self.cartable.send(self.cartable.after_purchase_method, self) if self.cartable.after_purchase_method
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
namespace :carter do
|
2
|
+
desc "cleans up expired carts - default is 30 days old but this can be changed by entering the command line argument days=60"
|
3
|
+
task :clear_expired_carts => :environment do
|
4
|
+
days = (ENV['days'] || 30).to_i.days
|
5
|
+
|
6
|
+
count = Cart.remove_carts(days)
|
7
|
+
|
8
|
+
|
9
|
+
puts "#{count} carts deleted."
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
if defined?(Rails)
|
2
|
+
require 'carter/engine' if Rails::VERSION::MAJOR == 3
|
3
|
+
ActiveRecord::Base.extend Carter::ActiveRecord::Cartable if defined?(ActiveRecord)
|
4
|
+
require 'carter/routing'
|
5
|
+
ActionController::Routing::RouteSet::Mapper.send :include, Carter::Routing::MapperExtensions
|
6
|
+
end
|
7
|
+
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CartItem do
|
4
|
+
before(){ Product.acts_as_cartable }
|
5
|
+
it {should belong_to :cart}
|
6
|
+
it {should belong_to :cartable }
|
7
|
+
it {should belong_to :owner }
|
8
|
+
|
9
|
+
describe "named scopes" do
|
10
|
+
let(:owner_1) { Factory(:user) }
|
11
|
+
let(:product_1){ Factory(:product) }
|
12
|
+
let(:product_2){ Factory(:product) }
|
13
|
+
let(:cart) { Factory(:cart) }
|
14
|
+
|
15
|
+
before(:each){
|
16
|
+
cart.add_item(product_1)
|
17
|
+
cart.add_item(product_1, 1, owner_1)
|
18
|
+
cart.add_item(product_2, 1, owner_1)
|
19
|
+
}
|
20
|
+
|
21
|
+
it "should find the cart_item by product and owner" do
|
22
|
+
cart.cart_items.for_cartable_and_owner(product_2, owner_1).size.should == 1
|
23
|
+
cart.cart_items.for_cartable_and_owner(product_2, owner_1).first.cartable.should == product_2
|
24
|
+
cart.cart_items.for_cartable_and_owner(product_2, owner_1).first.owner.should == owner_1
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should find the cart_item by product and owner is blank" do
|
28
|
+
cart.cart_items.for_cartable_and_owner(product_2, nil).size.should == 0
|
29
|
+
cart.cart_items.for_cartable_and_owner(product_1, nil).size.should == 1
|
30
|
+
cart.cart_items.for_cartable_and_owner(product_1, nil).first.cartable.should == product_1
|
31
|
+
cart.cart_items.for_cartable_and_owner(product_1, nil).first.owner.should == nil
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should find the cart_items by product" do
|
35
|
+
cart.cart_items.for_cartable(product_1).size.should == 2
|
36
|
+
cart.cart_items.for_cartable(product_1).first.cartable.should == product_1
|
37
|
+
cart.cart_items.for_cartable(product_1).last.cartable.should == product_1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "an instance" do
|
42
|
+
let(:cart){ Factory(:cart) }
|
43
|
+
let(:cart_item) {Factory(:cart_item, :cart => cart ) }
|
44
|
+
it "should respond to shopper" do
|
45
|
+
cart_item.should respond_to :shopper
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should delegate shopper to the cart" do
|
49
|
+
cart_item.shopper.should == cart.shopper
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "adding to a cart" do
|
54
|
+
let(:cart) {Factory(:cart)}
|
55
|
+
let(:cartable) {Factory(:product)}
|
56
|
+
|
57
|
+
it "should respond to add_item" do
|
58
|
+
cart.should respond_to :add_item
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should increment cart items by one" do
|
62
|
+
proc{ cart.add_item(cartable) }.should change(cart.cart_items, :size).by(1)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should bump the quatity value of an exisitng item if the cart_item already exists" do
|
66
|
+
cart.add_item(cartable)
|
67
|
+
cart.cart_items.size.should == 1
|
68
|
+
proc{ cart.add_item(cartable) }.should_not change(cart.cart_items, :size)
|
69
|
+
cart.total.should == (cartable.price * 2)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should remove the item if the quantity is 0" do
|
73
|
+
cart.add_item(cartable)
|
74
|
+
cart_item = cart.cart_items.first
|
75
|
+
lambda{ cart_item.update_attributes(:quantity => 0) }.should change(cart.cart_items, :size).from(1).to(0)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should remove the item if the quantity is less than zero" do
|
79
|
+
cart.add_item(cartable)
|
80
|
+
cart_item = cart.cart_items.first
|
81
|
+
lambda{ cart_item.update_attributes(:quantity => -1) }.should change(cart.cart_items, :size).from(1).to(0)
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "cart totals" do
|
85
|
+
before do
|
86
|
+
cart.add_item(cartable, 2)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should return the total price of the cart" do
|
90
|
+
cart.total.should == (2 * cartable.price)
|
91
|
+
cart.add_item(cartable)
|
92
|
+
cart.total.should == (3 * cartable.price)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Cart do
|
4
|
+
before(){ Product.acts_as_cartable }
|
5
|
+
it {should have_many :cart_items}
|
6
|
+
it {should belong_to :shopper }
|
7
|
+
|
8
|
+
describe "removing carts" do
|
9
|
+
let(:cartable) {Factory(:product)}
|
10
|
+
before do
|
11
|
+
Cart.destroy_all
|
12
|
+
@one_day_old = Factory(:cart, :updated_at => Time.now - 2.day)
|
13
|
+
@one_week_old = Factory(:cart, :updated_at => Time.now - 8.days)
|
14
|
+
@cart_item_one = Factory(:cart_item, :cart_id => @one_day_old.id)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should remove carts a week old" do
|
18
|
+
d = (Time.now.midnight - 7.to_i.days)
|
19
|
+
scoped_carts = mock("carts")
|
20
|
+
scoped_carts.should_receive(:find_in_batches).with(:conditions => ["updated_at < ?", d])
|
21
|
+
Cart.should_receive(:with_state).with(:active).and_return(scoped_carts)
|
22
|
+
Cart.remove_carts
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should remove carts a 2 days old" do
|
26
|
+
d = (Time.now.midnight - 2.to_i.days)
|
27
|
+
scoped_carts = mock("carts")
|
28
|
+
scoped_carts.should_receive(:find_in_batches).with(:conditions => ["updated_at < ?", d])
|
29
|
+
Cart.should_receive(:with_state).with(:active).and_return(scoped_carts)
|
30
|
+
Cart.remove_carts(2)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should delete the carts over a week old" do
|
34
|
+
proc{ Cart.remove_carts() }.should change(Cart, :count).by(-1)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should delete the carts over a day old" do
|
38
|
+
proc{ Cart.remove_carts(1) }.should change(Cart, :count).by(-2)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should not delete the any carts when not old enough" do
|
42
|
+
proc{ Cart.remove_carts(14) }.should_not change(Cart, :count)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should return the number of deleted carts" do
|
46
|
+
Cart.remove_carts(1).should == 2
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should remove carts with another state" do
|
50
|
+
d = (Time.now.midnight - 2.to_i.days)
|
51
|
+
scoped_carts = mock("carts")
|
52
|
+
scoped_carts.should_receive(:find_in_batches).with(:conditions => ["updated_at < ?", d])
|
53
|
+
Cart.should_receive(:with_state).with(:failed).and_return(scoped_carts)
|
54
|
+
Cart.remove_carts(2, :failed)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "adding to a cart" do
|
59
|
+
let(:cart) {Factory(:cart)}
|
60
|
+
let(:cartable) {Factory(:product)}
|
61
|
+
|
62
|
+
it "should respond to add_item" do
|
63
|
+
cart.should respond_to :add_item
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should respond to empty?" do
|
67
|
+
cart.should respond_to :empty?
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should be empty" do
|
71
|
+
cart.should be_empty
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should increment cart items by one" do
|
75
|
+
proc{ cart.add_item(cartable) }.should change(cart.cart_items, :size).by(1)
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "cart totals" do
|
79
|
+
before do
|
80
|
+
cart.add_item(cartable, 2)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should not be empty" do
|
84
|
+
cart.should_not be_empty
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should return the total price of the cart" do
|
88
|
+
cart.total.should == (2 * cartable.price)
|
89
|
+
cart.add_item(cartable)
|
90
|
+
cart.total.should == (3 * cartable.price)
|
91
|
+
end
|
92
|
+
|
93
|
+
# it "should have many cartables" do
|
94
|
+
# cart.cartables.should include(cartable)
|
95
|
+
# end
|
96
|
+
|
97
|
+
# it "should have many products" do
|
98
|
+
# cart.should respond_to(:products)
|
99
|
+
# end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|