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.
@@ -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,8 @@
1
+ require 'carter'
2
+ require 'rails'
3
+
4
+ module Carter
5
+ class Engine < Rails::Engine
6
+
7
+ end
8
+ end
@@ -0,0 +1,2 @@
1
+ require 'carter/errors/multiple_cart_items_not_allowed'
2
+ require 'carter/errors/setup_error'
@@ -0,0 +1,5 @@
1
+ module Carter
2
+ class MultipleCartItemsNotAllowed < StandardError
3
+
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Carter
2
+ class SetupError < StandardError
3
+
4
+ end
5
+ 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,11 @@
1
+ module Carter #:nodoc:
2
+ module Routing #:nodoc:
3
+ module MapperExtensions
4
+ def carter_routes(options={})
5
+ self.resources :carts
6
+ self.resources :cart_items
7
+
8
+ end
9
+ end
10
+ end
11
+ 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