spree_promo 0.70.7 → 1.0.0.rc1
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 +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)
|