spree_promo 0.70.7 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +2 -2
- data/app/assets/javascripts/admin/promotions.js +3 -23
- data/app/controllers/spree/admin/promotion_actions_controller.rb +27 -0
- data/app/controllers/spree/admin/promotion_rules_controller.rb +26 -0
- data/app/controllers/spree/admin/promotions_controller.rb +24 -0
- data/app/controllers/spree/checkout_controller_decorator.rb +39 -0
- data/app/controllers/{content_controller_decorator.rb → spree/content_controller_decorator.rb} +4 -2
- data/app/controllers/{orders_controller_decorator.rb → spree/orders_controller_decorator.rb} +1 -3
- data/app/models/{adjustment_decorator.rb → spree/adjustment_decorator.rb} +1 -1
- data/app/models/spree/calculator/free_shipping.rb +18 -0
- data/app/models/{order_decorator.rb → spree/order_decorator.rb} +5 -4
- data/app/models/{product_decorator.rb → spree/product_decorator.rb} +3 -3
- data/app/models/spree/product_group_decorator.rb +3 -0
- data/app/models/spree/promotion/actions/create_adjustment.rb +33 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +28 -0
- data/app/models/spree/promotion/rules/first_order.rb +7 -0
- data/app/models/spree/promotion/rules/item_total.rb +14 -0
- data/app/models/spree/promotion/rules/landing_page.rb +16 -0
- data/app/models/spree/promotion/rules/product.rb +40 -0
- data/app/models/spree/promotion/rules/user.rb +18 -0
- data/app/models/spree/promotion/rules/user_logged_in.rb +15 -0
- data/app/models/spree/promotion.rb +131 -0
- data/app/models/spree/promotion_action.rb +17 -0
- data/app/models/spree/promotion_action_line_item.rb +6 -0
- data/app/models/spree/promotion_rule.rb +12 -0
- data/app/overrides/promo_admin_tabs.rb +2 -2
- data/app/overrides/promo_coupon_code_field.rb +2 -2
- data/app/overrides/promo_product_properties.rb +2 -2
- data/app/views/{admin → spree/admin}/promotion_actions/create.js.erb +1 -1
- data/app/views/{admin → spree/admin}/promotion_actions/destroy.js.erb +0 -0
- data/app/views/{admin → spree/admin}/promotion_rules/create.js.erb +1 -1
- data/app/views/{admin → spree/admin}/promotion_rules/destroy.js.erb +0 -0
- data/app/views/{admin → spree/admin}/promotions/_actions.html.erb +5 -5
- data/app/views/{admin → spree/admin}/promotions/_form.html.erb +11 -11
- data/app/views/{admin → spree/admin}/promotions/_promotion_action.html.erb +2 -2
- data/app/views/{admin → spree/admin}/promotions/_promotion_rule.html.erb +2 -2
- data/app/views/{admin → spree/admin}/promotions/_rules.html.erb +2 -2
- data/app/views/{admin → spree/admin}/promotions/_tab.html.erb +0 -0
- data/app/views/{admin → spree/admin}/promotions/actions/_create_adjustment.html.erb +1 -1
- data/app/views/{admin → spree/admin}/promotions/actions/_create_line_items.html.erb +1 -0
- data/app/views/{admin → spree/admin}/promotions/actions/_give_store_credit.html.erb +1 -1
- data/app/views/{admin → spree/admin}/promotions/edit.html.erb +3 -3
- data/app/views/{admin → spree/admin}/promotions/index.html.erb +2 -2
- data/app/views/{admin → spree/admin}/promotions/new.html.erb +1 -1
- data/app/views/{admin → spree/admin}/promotions/rules/_first_order.html.erb +0 -0
- data/app/views/{admin → spree/admin}/promotions/rules/_item_total.html.erb +1 -1
- data/app/views/{admin → spree/admin}/promotions/rules/_landing_page.html.erb +0 -0
- data/app/views/{admin → spree/admin}/promotions/rules/_product.html.erb +2 -2
- data/app/views/{admin → spree/admin}/promotions/rules/_user.html.erb +0 -0
- data/app/views/{admin → spree/admin}/promotions/rules/_user_logged_in.html.erb +0 -0
- data/app/views/{checkout → spree/checkout}/_coupon_code_field.html.erb +1 -1
- data/app/views/{products → spree/products}/_promotions.html.erb +0 -0
- data/config/locales/en.yml +7 -0
- data/config/routes.rb +1 -1
- data/db/migrate/20100419190933_rename_coupons_to_promotions.rb +4 -4
- data/db/migrate/20100426100726_create_promotion_rules.rb +4 -4
- data/db/migrate/20100501084722_match_policy_for_promotions.rb +3 -7
- data/db/migrate/20100501095202_create_promotion_rules_users.rb +3 -3
- data/db/migrate/20100502163939_name_for_promotions.rb +3 -7
- data/db/migrate/20100923095305_update_calculable_type_for_promotions.rb +4 -4
- data/db/migrate/20101026184833_migrate_adjustments.rb +4 -4
- data/db/migrate/20110331094351_promotion_changes_to_subclass_of_activator.rb +13 -13
- data/db/migrate/20110404123358_create_promotion_actions.rb +2 -6
- data/db/migrate/20110601202923_create_promotion_action_line_items.rb +4 -6
- data/db/migrate/20111013155037_namespace_promo_tables.rb +9 -0
- data/lib/spree/promo/engine.rb +59 -0
- data/lib/spree/promo/environment.rb +1 -1
- data/lib/spree/promo.rb +10 -0
- data/lib/spree_promo.rb +1 -71
- metadata +77 -72
- data/app/controllers/admin/promotion_actions_controller.rb +0 -28
- data/app/controllers/admin/promotion_rules_controller.rb +0 -26
- data/app/controllers/admin/promotions_controller.rb +0 -21
- data/app/controllers/checkout_controller_decorator.rb +0 -13
- data/app/models/calculator/free_shipping.rb +0 -19
- data/app/models/pending_promotion.rb +0 -4
- data/app/models/product_group_decorator.rb +0 -3
- data/app/models/promotion/actions/create_adjustment.rb +0 -28
- data/app/models/promotion/actions/create_line_items.rb +0 -29
- data/app/models/promotion/rules/first_order.rb +0 -7
- data/app/models/promotion/rules/item_total.rb +0 -13
- data/app/models/promotion/rules/landing_page.rb +0 -17
- data/app/models/promotion/rules/product.rb +0 -40
- data/app/models/promotion/rules/user.rb +0 -17
- data/app/models/promotion/rules/user_logged_in.rb +0 -13
- data/app/models/promotion.rb +0 -97
- data/app/models/promotion_action.rb +0 -17
- data/app/models/promotion_action_line_item.rb +0 -6
- data/app/models/promotion_rule.rb +0 -11
- data/app/models/user_decorator.rb +0 -4
- data/config/cucumber.yml +0 -10
- data/db/migrate/20120119024708_create_pending_promotions.rb +0 -11
- data/lib/tasks/promotions.rake +0 -11
- data/lib/tasks/promotions_extension_tasks.rake +0 -17
data/LICENSE
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c) 2007-2011,
|
1
|
+
Copyright (c) 2007-2011, Spree Commerce, Inc. and other contributors
|
2
2
|
All rights reserved.
|
3
3
|
|
4
4
|
Redistribution and use in source and binary forms, with or without modification,
|
@@ -23,4 +23,4 @@ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
23
23
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
24
24
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
25
25
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
26
|
-
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
26
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@@ -3,7 +3,7 @@ var initProductRuleSourceField = function(){
|
|
3
3
|
|
4
4
|
$products_source_field = jQuery('.products_rule_products_source_field input');
|
5
5
|
$products_source_field.click(function() {
|
6
|
-
$rule_container = jQuery(this).parents('.
|
6
|
+
$rule_container = jQuery(this).parents('.promotion-rule');
|
7
7
|
if(this.checked){
|
8
8
|
if(this.value == 'manual'){
|
9
9
|
$rule_container.find('.products_rule_products').show();
|
@@ -48,26 +48,6 @@ var initProductActions = function(){
|
|
48
48
|
// CreateLineItems Promotion Action
|
49
49
|
//
|
50
50
|
( function(){
|
51
|
-
// Autocomplete product and populate variant select
|
52
|
-
if($('.promotion_action.create_line_items ').is('*')){
|
53
|
-
$(".promotion_action.create_line_items input[name='add_product_name']").autocomplete("/admin/products.json?authenticity_token=" + $('meta[name=csrf-token]').attr("content"), {
|
54
|
-
parse: prep_product_autocomplete_data,
|
55
|
-
formatItem: function(item) {
|
56
|
-
return format_product_autocomplete(item);
|
57
|
-
}
|
58
|
-
}).result(function(event, data, formatted) {
|
59
|
-
if(data){
|
60
|
-
if(data['variant']==undefined){
|
61
|
-
// product
|
62
|
-
$('#add_line_item_variant_id').val(data['product']['master']['id']);
|
63
|
-
}else{
|
64
|
-
// variant
|
65
|
-
$('#add_line_item_variant_id').val(data['variant']['id']);
|
66
|
-
}
|
67
|
-
}
|
68
|
-
});
|
69
|
-
}
|
70
|
-
|
71
51
|
var hideOrShowItemTables = function(){
|
72
52
|
$('.promotion_action table').each(function(){
|
73
53
|
if($(this).find('td').length == 0){
|
@@ -96,10 +76,10 @@ var initProductActions = function(){
|
|
96
76
|
};
|
97
77
|
setupRemoveLineItems();
|
98
78
|
// Add line item to list
|
99
|
-
$(".promotion_action.create_line_items button.add").live('click',function(){
|
79
|
+
$(".promotion_action.create_line_items button.add").live('click',function(e){
|
100
80
|
var $container = $(this).parents('.promotion_action');
|
101
81
|
var product_name = $container.find("input[name='add_product_name']").val();
|
102
|
-
var variant_id = $container.find("input[name='
|
82
|
+
var variant_id = $container.find("input[name='add_variant_id']").val();
|
103
83
|
var quantity = $container.find("input[name='add_quantity']").val();
|
104
84
|
if(variant_id){
|
105
85
|
// Add to the table
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Spree::Admin::PromotionActionsController < Spree::Admin::BaseController
|
2
|
+
def create
|
3
|
+
@calculators = Spree::Promotion::Actions::CreateAdjustment.calculators
|
4
|
+
@promotion = Spree::Promotion.find(params[:promotion_id])
|
5
|
+
@promotion_action = params[:action_type].constantize.new(params[:promotion_action])
|
6
|
+
@promotion_action.promotion = @promotion
|
7
|
+
if @promotion_action.save
|
8
|
+
flash.notice = I18n.t(:successfully_created, :resource => I18n.t(:promotion_action))
|
9
|
+
end
|
10
|
+
respond_to do |format|
|
11
|
+
format.html { redirect_to spree.edit_admin_promotion_path(@promotion)}
|
12
|
+
format.js { render :layout => false }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def destroy
|
17
|
+
@promotion = Spree::Promotion.find(params[:promotion_id])
|
18
|
+
@promotion_action = @promotion.promotion_actions.find(params[:id])
|
19
|
+
if @promotion_action.destroy
|
20
|
+
flash.notice = I18n.t(:successfully_removed, :resource => I18n.t(:promotion_action))
|
21
|
+
end
|
22
|
+
respond_to do |format|
|
23
|
+
format.html { redirect_to spree.edit_admin_promotion_path(@promotion)}
|
24
|
+
format.js { render :layout => false }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class Spree::Admin::PromotionRulesController < Spree::Admin::BaseController
|
2
|
+
def create
|
3
|
+
@promotion = Spree::Promotion.find(params[:promotion_id])
|
4
|
+
@promotion_rule = params[:promotion_rule][:type].constantize.new(params[:promotion_rule])
|
5
|
+
@promotion_rule.promotion = @promotion
|
6
|
+
if @promotion_rule.save
|
7
|
+
flash.notice = I18n.t(:successfully_created, :resource => I18n.t(:promotion_rule))
|
8
|
+
end
|
9
|
+
respond_to do |format|
|
10
|
+
format.html { redirect_to spree.edit_admin_promotion_path(@promotion)}
|
11
|
+
format.js { render :layout => false }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def destroy
|
16
|
+
@promotion = Spree::Promotion.find(params[:promotion_id])
|
17
|
+
@promotion_rule = @promotion.promotion_rules.find(params[:id])
|
18
|
+
if @promotion_rule.destroy
|
19
|
+
flash.notice = I18n.t(:successfully_removed, :resource => I18n.t(:promotion_rule))
|
20
|
+
end
|
21
|
+
respond_to do |format|
|
22
|
+
format.html { redirect_to spree.edit_admin_promotion_path(@promotion)}
|
23
|
+
format.js { render :layout => false }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Spree
|
2
|
+
module Admin
|
3
|
+
class PromotionsController < ResourceController
|
4
|
+
before_filter :load_data
|
5
|
+
|
6
|
+
protected
|
7
|
+
def build_resource
|
8
|
+
@promotion = Promotion.new(params[:promotion])
|
9
|
+
if params[:promotion] && params[:promotion][:calculator_type]
|
10
|
+
@promotion.calculator = params[:promotion][:calculator_type].constantize.new
|
11
|
+
end
|
12
|
+
@promotion
|
13
|
+
end
|
14
|
+
|
15
|
+
def location_after_save
|
16
|
+
spree.edit_admin_promotion_url(@promotion)
|
17
|
+
end
|
18
|
+
|
19
|
+
def load_data
|
20
|
+
@calculators = Rails.application.config.spree.calculators.promotion_actions_create_adjustments
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
Spree::CheckoutController.class_eval do
|
2
|
+
def update
|
3
|
+
if @order.update_attributes(object_params)
|
4
|
+
|
5
|
+
fire_event('spree.checkout.update')
|
6
|
+
|
7
|
+
if @order.coupon_code.present?
|
8
|
+
# Promotion codes are stored in the preference table
|
9
|
+
# Therefore we need to do a lookup there and find if one exists
|
10
|
+
if Spree::Preference.where(:value => @order.coupon_code).where("key LIKE 'spree/promotion/code/%'").present?
|
11
|
+
fire_event('spree.checkout.coupon_code_added', :coupon_code => @order.coupon_code)
|
12
|
+
# If it doesn't exist, raise an error!
|
13
|
+
# Giving them another chance to enter a valid coupon code
|
14
|
+
else
|
15
|
+
flash[:error] = t(:promotion_not_found)
|
16
|
+
render :edit and return
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
if @order.next
|
21
|
+
state_callback(:after)
|
22
|
+
else
|
23
|
+
flash[:error] = t(:payment_processing_failed)
|
24
|
+
respond_with(@order, :location => checkout_state_path(@order.state))
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
if @order.state == 'complete' || @order.completed?
|
29
|
+
flash.notice = t(:order_processed_successfully)
|
30
|
+
flash[:commerce_tracking] = 'nothing special'
|
31
|
+
respond_with(@order, :location => completion_route)
|
32
|
+
else
|
33
|
+
respond_with(@order, :location => checkout_state_path(@order.state))
|
34
|
+
end
|
35
|
+
else
|
36
|
+
respond_with(@order) { |format| format.html { render :edit } }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/app/controllers/{content_controller_decorator.rb → spree/content_controller_decorator.rb}
RENAMED
@@ -1,8 +1,10 @@
|
|
1
1
|
# Keep a record ot all static page paths visited for promotions that require them
|
2
|
-
ContentController.class_eval do
|
2
|
+
Spree::ContentController.class_eval do
|
3
3
|
after_filter :store_visited_path
|
4
|
+
|
4
5
|
def store_visited_path
|
5
6
|
session[:visited_paths] ||= []
|
6
|
-
|
7
|
+
path = "content/#{params[:action]}"
|
8
|
+
session[:visited_paths] = (session[:visited_paths] << path).compact.uniq
|
7
9
|
end
|
8
10
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Spree
|
2
|
+
class Calculator::FreeShipping < Calculator
|
3
|
+
def self.description
|
4
|
+
I18n.t(:free_shipping)
|
5
|
+
end
|
6
|
+
|
7
|
+
def compute(object)
|
8
|
+
if object.is_a?(Array)
|
9
|
+
return if object.empty?
|
10
|
+
order = object.first.order
|
11
|
+
else
|
12
|
+
order = object
|
13
|
+
end
|
14
|
+
|
15
|
+
order.ship_total
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,8 +1,10 @@
|
|
1
|
-
Order.class_eval do
|
2
|
-
|
1
|
+
Spree::Order.class_eval do
|
3
2
|
attr_accessible :coupon_code
|
4
3
|
attr_accessor :coupon_code
|
5
4
|
|
5
|
+
# Tells us if there if the specified promotion is already associated with the order
|
6
|
+
# regardless of whether or not its currently eligible. Useful because generally
|
7
|
+
# you would only want a promotion to apply to order no more than once.
|
6
8
|
def promotion_credit_exists?(promotion)
|
7
9
|
!! adjustments.promotion.reload.detect { |credit| credit.originator.promotion.id == promotion.id }
|
8
10
|
end
|
@@ -11,7 +13,7 @@ Order.class_eval do
|
|
11
13
|
line_items.map {|li| li.variant.product}
|
12
14
|
end
|
13
15
|
|
14
|
-
unless self.
|
16
|
+
unless self.method_defined?('update_adjustments_with_promotion_limiting')
|
15
17
|
def update_adjustments_with_promotion_limiting
|
16
18
|
update_adjustments_without_promotion_limiting
|
17
19
|
return if adjustments.promotion.eligible.none?
|
@@ -20,5 +22,4 @@ Order.class_eval do
|
|
20
22
|
end
|
21
23
|
alias_method_chain :update_adjustments, :promotion_limiting
|
22
24
|
end
|
23
|
-
|
24
25
|
end
|
@@ -1,10 +1,10 @@
|
|
1
|
-
Product.class_eval do
|
2
|
-
has_and_belongs_to_many :promotion_rules
|
1
|
+
Spree::Product.class_eval do
|
2
|
+
has_and_belongs_to_many :promotion_rules, :join_table => 'spree_products_promotion_rules'
|
3
3
|
|
4
4
|
def possible_promotions
|
5
5
|
rules_with_matching_product_groups = product_groups.map(&:promotion_rules).flatten
|
6
6
|
all_rules = promotion_rules + rules_with_matching_product_groups
|
7
7
|
promotion_ids = all_rules.map(&:activator_id).uniq
|
8
|
-
Promotion.advertised.where(:id => promotion_ids)
|
8
|
+
Spree::Promotion.advertised.where(:id => promotion_ids)
|
9
9
|
end
|
10
10
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Spree
|
2
|
+
class Promotion::Actions::CreateAdjustment < PromotionAction
|
3
|
+
calculated_adjustments
|
4
|
+
|
5
|
+
delegate :eligible?, :to => :promotion
|
6
|
+
|
7
|
+
before_validation :ensure_action_has_calculator
|
8
|
+
|
9
|
+
def perform(options = {})
|
10
|
+
return unless order = options[:order]
|
11
|
+
# Nothing to do if the promotion is already associated with the order
|
12
|
+
return if order.promotion_credit_exists?(promotion)
|
13
|
+
if amount = calculator.compute(order)
|
14
|
+
amount = BigDecimal.new(amount.to_s)
|
15
|
+
amount = order.item_total if amount > order.item_total
|
16
|
+
order.adjustments.promotion.reload.clear
|
17
|
+
order.update!
|
18
|
+
create_adjustment("#{I18n.t(:promotion)} (#{promotion.name})", order, order)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Ensure a negative amount which does not exceed the sum of the order's item_total and ship_total
|
23
|
+
def compute_amount(calculable)
|
24
|
+
[(calculable.item_total + calculable.ship_total), super.to_f.abs].min * -1
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def ensure_action_has_calculator
|
29
|
+
return if self.calculator
|
30
|
+
self.calculator = Calculator::FlatPercentItemTotal.new
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Spree
|
2
|
+
class Promotion::Actions::CreateLineItems < PromotionAction
|
3
|
+
has_many :promotion_action_line_items, :foreign_key => 'promotion_action_id'
|
4
|
+
|
5
|
+
attr_accessor :line_items_string
|
6
|
+
|
7
|
+
def perform(options = {})
|
8
|
+
return unless order = options[:order]
|
9
|
+
promotion_action_line_items.each do |item|
|
10
|
+
order.add_variant(item.variant, item.quantity)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def line_items_string
|
15
|
+
promotion_action_line_items.map { |li| "#{li.variant_id}x#{li.quantity}" }.join(',')
|
16
|
+
end
|
17
|
+
|
18
|
+
def line_items_string=(value)
|
19
|
+
promotion_action_line_items.destroy_all
|
20
|
+
value.to_s.split(',').each do |str|
|
21
|
+
variant_id, quantity = str.split('x')
|
22
|
+
if variant_id && quantity && variant = Variant.find_by_id(variant_id)
|
23
|
+
promotion_action_line_items.create(:variant => variant, :quantity => quantity.to_i)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# A rule to limit a promotion to a specific user.
|
2
|
+
module Spree
|
3
|
+
class Promotion::Rules::ItemTotal < PromotionRule
|
4
|
+
preference :amount, :decimal, :default => 100.00
|
5
|
+
preference :operator, :string, :default => '>'
|
6
|
+
|
7
|
+
OPERATORS = ['gt', 'gte']
|
8
|
+
|
9
|
+
def eligible?(order, options = {})
|
10
|
+
item_total = order.line_items.map(&:amount).sum
|
11
|
+
item_total.send(preferred_operator == 'gte' ? :>= : :>, BigDecimal.new(preferred_amount.to_s))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# A rule to limit a promotion to a specific user.
|
2
|
+
module Spree
|
3
|
+
class Promotion::Rules::LandingPage < PromotionRule
|
4
|
+
preference :path, :string
|
5
|
+
|
6
|
+
def eligible?(order, options = {})
|
7
|
+
if options.has_key?(:visited_paths)
|
8
|
+
options[:visited_paths].to_a.any? do |path|
|
9
|
+
path == preferred_path
|
10
|
+
end
|
11
|
+
else
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# A rule to limit a promotion based on products in the order.
|
2
|
+
# Can require all or any of the products to be present.
|
3
|
+
# Valid products either come from assigned product group or are assingned directly to the rule.
|
4
|
+
module Spree
|
5
|
+
class Promotion::Rules::Product < PromotionRule
|
6
|
+
belongs_to :product_group
|
7
|
+
has_and_belongs_to_many :products, :class_name => '::Spree::Product', :join_table => 'spree_products_promotion_rules', :foreign_key => 'promotion_rule_id'
|
8
|
+
|
9
|
+
MATCH_POLICIES = %w(any all)
|
10
|
+
preference :match_policy, :string, :default => MATCH_POLICIES.first
|
11
|
+
|
12
|
+
# scope/association that is used to test eligibility
|
13
|
+
def eligible_products
|
14
|
+
product_group ? product_group.products : products
|
15
|
+
end
|
16
|
+
|
17
|
+
def eligible?(order, options = {})
|
18
|
+
return true if eligible_products.empty?
|
19
|
+
if preferred_match_policy == 'all'
|
20
|
+
eligible_products.all? {|p| order.products.include?(p) }
|
21
|
+
else
|
22
|
+
order.products.any? {|p| eligible_products.include?(p) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def products_source=(source)
|
27
|
+
if source.to_s == 'manual'
|
28
|
+
self.product_group_id = nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def product_ids_string
|
33
|
+
product_ids.join(',')
|
34
|
+
end
|
35
|
+
|
36
|
+
def product_ids_string=(s)
|
37
|
+
self.product_ids = s.to_s.split(',').map(&:strip)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Spree
|
2
|
+
class Promotion::Rules::User < PromotionRule
|
3
|
+
belongs_to :user
|
4
|
+
has_and_belongs_to_many :users, :class_name => '::Spree::User', :join_table => 'spree_promotion_rules_users', :foreign_key => 'promotion_rule_id'
|
5
|
+
|
6
|
+
def eligible?(order, options = {})
|
7
|
+
users.none? or users.include?(order.user)
|
8
|
+
end
|
9
|
+
|
10
|
+
def user_ids_string
|
11
|
+
user_ids.join(',')
|
12
|
+
end
|
13
|
+
|
14
|
+
def user_ids_string=(s)
|
15
|
+
self.user_ids = s.to_s.split(',').map(&:strip)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Spree
|
2
|
+
class Promotion::Rules::UserLoggedIn < PromotionRule
|
3
|
+
def eligible?(order, options = {})
|
4
|
+
|
5
|
+
# this is tricky. We couldn't use any of the devise methods since we aren't in the controller.
|
6
|
+
# we need to rely on the controller already having done this for us.
|
7
|
+
|
8
|
+
# The thinking is that the controller should have some sense of what state
|
9
|
+
# we should be in before firing events,
|
10
|
+
# so the controller will have to set this field.
|
11
|
+
|
12
|
+
return options && options[:user_signed_in]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Spree
|
2
|
+
class Promotion < Spree::Activator
|
3
|
+
MATCH_POLICIES = %w(all any)
|
4
|
+
|
5
|
+
preference :usage_limit, :integer
|
6
|
+
preference :match_policy, :string, :default => MATCH_POLICIES.first
|
7
|
+
preference :code, :string
|
8
|
+
preference :advertise, :boolean, :default => false
|
9
|
+
|
10
|
+
[:usage_limit, :match_policy, :code, :advertise].each do |field|
|
11
|
+
alias_method field, "preferred_#{field}"
|
12
|
+
alias_method "#{field}=", "preferred_#{field}="
|
13
|
+
end
|
14
|
+
|
15
|
+
has_many :promotion_rules, :foreign_key => 'activator_id', :autosave => true, :dependent => :destroy
|
16
|
+
alias_method :rules, :promotion_rules
|
17
|
+
accepts_nested_attributes_for :promotion_rules
|
18
|
+
|
19
|
+
has_many :promotion_actions, :foreign_key => 'activator_id', :autosave => true, :dependent => :destroy
|
20
|
+
alias_method :actions, :promotion_actions
|
21
|
+
accepts_nested_attributes_for :promotion_actions
|
22
|
+
|
23
|
+
after_create :update_preferences
|
24
|
+
|
25
|
+
# TODO: This shouldn't be necessary with :autosave option but nested attribute updating of actions is broken without it
|
26
|
+
after_save :save_rules_and_actions
|
27
|
+
def save_rules_and_actions
|
28
|
+
(rules + actions).each &:save
|
29
|
+
end
|
30
|
+
|
31
|
+
validates :name, :presence => true
|
32
|
+
#validates :preferred_code, :presence => true, :if => lambda{|r| r.event_name == 'spree.checkout.coupon_code_added' }
|
33
|
+
|
34
|
+
%w(usage_limit match_policy code advertise).each do |pref|
|
35
|
+
method_name = pref.to_sym
|
36
|
+
define_method method_name do
|
37
|
+
get_preference(pref.to_sym)
|
38
|
+
end
|
39
|
+
|
40
|
+
method_name = "#{pref}=".to_sym
|
41
|
+
define_method method_name do |value|
|
42
|
+
unless new_record?
|
43
|
+
pref = value
|
44
|
+
else
|
45
|
+
@preferences_hash ||= {}
|
46
|
+
@preferences_hash[pref.to_sym] = value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class << self
|
52
|
+
def advertised
|
53
|
+
#TODO this is broken because the new preferences aren't a direct relationship returning
|
54
|
+
#all for now
|
55
|
+
where(true)
|
56
|
+
#includes(:stored_preferences)
|
57
|
+
#includes(:stored_preferences).where(:spree_preferences => {:name => 'advertise', :value => '1'})
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# TODO: Remove that after fix for https://rails.lighthouseapp.com/projects/8994/tickets/4329-has_many-through-association-does-not-link-models-on-association-save
|
62
|
+
# is provided
|
63
|
+
def save(*)
|
64
|
+
if super
|
65
|
+
promotion_rules.each { |p| p.save }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def activate(payload)
|
70
|
+
# Since multiple promotions could be listening we need to make sure the
|
71
|
+
# event applies to this one.
|
72
|
+
if eligible?(payload[:order], payload)
|
73
|
+
actions.each do |action|
|
74
|
+
action.perform(payload)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Whether the promotion is eligible for this particular order.
|
80
|
+
def eligible?(order, options = {})
|
81
|
+
return false if expired? || usage_limit_exceeded?(order)
|
82
|
+
if event_code = options[:coupon_code].to_s.strip.downcase
|
83
|
+
return false unless event_code == self.code.to_s.strip.downcase
|
84
|
+
end
|
85
|
+
rules_are_eligible?(order, options)
|
86
|
+
end
|
87
|
+
|
88
|
+
def rules_are_eligible?(order, options = {})
|
89
|
+
return true if rules.none?
|
90
|
+
if match_policy == 'all'
|
91
|
+
rules.all?{|r| r.eligible?(order, options)}
|
92
|
+
else
|
93
|
+
rules.any?{|r| r.eligible?(order, options)}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Products assigned to all product rules
|
98
|
+
def products
|
99
|
+
@products ||= rules.of_type('Promotion::Rules::Product').map(&:products).flatten.uniq
|
100
|
+
end
|
101
|
+
|
102
|
+
def usage_limit_exceeded?(order = nil)
|
103
|
+
preferred_usage_limit.present? && preferred_usage_limit.to_i > 0 && adjusted_credits_count(order) >= preferred_usage_limit.to_i
|
104
|
+
end
|
105
|
+
|
106
|
+
def adjusted_credits_count(order)
|
107
|
+
return credits_count if order.nil?
|
108
|
+
credits_count - (order.promotion_credit_exists?(self) ? 1 : 0)
|
109
|
+
end
|
110
|
+
|
111
|
+
def credits
|
112
|
+
Adjustment.promotion.where(:originator_id => actions.map(&:id))
|
113
|
+
end
|
114
|
+
|
115
|
+
def credits_count
|
116
|
+
credits.count
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def update_preferences
|
123
|
+
if @preferences_hash.present? && !@preferences_hash.empty?
|
124
|
+
@preferences_hash.each do |key, value|
|
125
|
+
pref_key = "spree/promotion/#{key}/#{self.id}"
|
126
|
+
Spree::Preference.create(:value => value, :key => pref_key)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Base class for all types of promotion action.
|
2
|
+
# PromotionActions perform the necessary tasks when a promotion is activated by an event and determined to be eligible.
|
3
|
+
module Spree
|
4
|
+
class PromotionAction < ActiveRecord::Base
|
5
|
+
belongs_to :promotion, :foreign_key => 'activator_id'
|
6
|
+
|
7
|
+
scope :of_type, lambda {|t| {:conditions => {:type => t}}}
|
8
|
+
|
9
|
+
# This method should be overriden in subclass
|
10
|
+
# Updates the state of the order or performs some other action depending on the subclass
|
11
|
+
# options will contain the payload from the event that activated the promotion. This will include
|
12
|
+
# the key :user which allows user based actions to be performed in addition to actions on the order
|
13
|
+
def perform(options = {})
|
14
|
+
raise 'perform should be implemented in a sub-class of PromotionAction'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# Base class for all promotion rules
|
2
|
+
module Spree
|
3
|
+
class PromotionRule < ActiveRecord::Base
|
4
|
+
belongs_to :promotion, :foreign_key => 'activator_id'
|
5
|
+
|
6
|
+
scope :of_type, lambda {|t| {:conditions => {:type => t}}}
|
7
|
+
|
8
|
+
def eligible?(order, options = {})
|
9
|
+
raise 'eligible? should be implemented in a sub-class of Promotion::PromotionRule'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
Deface::Override.new(:virtual_path => "layouts/admin",
|
1
|
+
Deface::Override.new(:virtual_path => "spree/layouts/admin",
|
2
2
|
:name => "promo_admin_tabs",
|
3
3
|
:insert_bottom => "[data-hook='admin_tabs'], #admin_tabs[data-hook]",
|
4
|
-
:text => "<%= tab(:promotions) %>",
|
4
|
+
:text => "<%= tab(:promotions, :url => spree.admin_promotions_path) %>",
|
5
5
|
:disabled => false)
|
@@ -1,5 +1,5 @@
|
|
1
|
-
Deface::Override.new(:virtual_path => "checkout/_payment",
|
1
|
+
Deface::Override.new(:virtual_path => "spree/checkout/_payment",
|
2
2
|
:name => "promo_coupon_code_field",
|
3
3
|
:replace => "[data-hook='coupon_code_field'], #coupon_code_field[data-hook]",
|
4
|
-
:partial => "checkout/coupon_code_field",
|
4
|
+
:partial => "spree/checkout/coupon_code_field",
|
5
5
|
:disabled => false)
|
@@ -1,5 +1,5 @@
|
|
1
|
-
Deface::Override.new(:virtual_path => "products/show",
|
1
|
+
Deface::Override.new(:virtual_path => "spree/products/show",
|
2
2
|
:name => "promo_product_properties",
|
3
3
|
:insert_after => "[data-hook='product_properties'], #product_properties[data-hook]",
|
4
|
-
:partial => "products/promotions",
|
4
|
+
:partial => "spree/products/promotions",
|
5
5
|
:disabled => false)
|