spree_promo 0.60.6 → 0.70.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.
Files changed (58) hide show
  1. data/LICENSE +26 -0
  2. data/app/assets/javascripts/admin/promotions.js +131 -0
  3. data/app/assets/javascripts/admin/spree_promo.js +3 -0
  4. data/app/assets/javascripts/store/spree_promo.js +2 -0
  5. data/{public → app/assets}/stylesheets/admin/promotions.css +19 -29
  6. data/app/assets/stylesheets/admin/spree_promo.css +5 -0
  7. data/app/assets/stylesheets/store/spree_promo.css +4 -0
  8. data/app/controllers/admin/promotion_actions_controller.rb +28 -0
  9. data/app/controllers/admin/promotions_controller.rb +2 -2
  10. data/app/controllers/content_controller_decorator.rb +8 -0
  11. data/app/controllers/orders_controller_decorator.rb +14 -5
  12. data/app/models/adjustment_decorator.rb +3 -0
  13. data/app/models/order_decorator.rb +24 -0
  14. data/app/models/product_decorator.rb +10 -0
  15. data/app/models/product_group_decorator.rb +3 -0
  16. data/app/models/promotion.rb +61 -41
  17. data/app/models/promotion/actions/create_adjustment.rb +28 -0
  18. data/app/models/promotion/actions/create_line_items.rb +29 -0
  19. data/app/models/promotion/rules/first_order.rb +1 -1
  20. data/app/models/promotion/rules/item_total.rb +3 -2
  21. data/app/models/promotion/rules/landing_page.rb +17 -0
  22. data/app/models/promotion/rules/product.rb +1 -1
  23. data/app/models/promotion/rules/user.rb +1 -1
  24. data/app/models/promotion/rules/user_logged_in.rb +13 -0
  25. data/app/models/promotion_action.rb +19 -0
  26. data/app/models/promotion_action_line_item.rb +6 -0
  27. data/app/models/promotion_rule.rb +3 -16
  28. data/app/overrides/promo_admin_tabs.rb +5 -0
  29. data/app/overrides/promo_coupon_code_field.rb +5 -0
  30. data/app/overrides/promo_product_properties.rb +5 -0
  31. data/app/views/admin/promotion_actions/create.js.erb +7 -0
  32. data/app/views/admin/promotion_actions/destroy.js.erb +2 -0
  33. data/app/views/admin/promotions/_actions.html.erb +32 -0
  34. data/app/views/admin/promotions/_form.html.erb +17 -17
  35. data/app/views/admin/promotions/_promotion_action.html.erb +12 -0
  36. data/app/views/admin/promotions/_promotion_rule.html.erb +2 -2
  37. data/app/views/admin/promotions/_rules.html.erb +32 -0
  38. data/app/views/admin/promotions/actions/_create_adjustment.html.erb +19 -0
  39. data/app/views/admin/promotions/actions/_create_line_items.html.erb +52 -0
  40. data/app/views/admin/promotions/actions/_give_store_credit.html.erb +6 -0
  41. data/app/views/admin/promotions/edit.html.erb +4 -38
  42. data/app/views/admin/promotions/index.html.erb +5 -7
  43. data/app/views/admin/promotions/rules/_landing_page.html.erb +8 -0
  44. data/app/views/admin/promotions/rules/_user_logged_in.html.erb +3 -0
  45. data/app/views/checkout/_coupon_code_field.html.erb +1 -1
  46. data/config/locales/en.yml +27 -5
  47. data/config/routes.rb +1 -0
  48. data/db/migrate/20101026184833_migrate_adjustments.rb +9 -0
  49. data/db/migrate/20110331094351_promotion_changes_to_subclass_of_activator.rb +22 -0
  50. data/db/migrate/20110404123358_create_promotion_actions.rb +13 -0
  51. data/db/migrate/20110601202923_create_promotion_action_line_items.rb +12 -0
  52. data/lib/spree_promo.rb +24 -100
  53. metadata +77 -26
  54. data/app/models/promotion_credit.rb +0 -9
  55. data/db/migrate/20100419194457_fix_existing_coupon_credits.rb +0 -9
  56. data/lib/spree_promo_hooks.rb +0 -11
  57. data/lib/tasks/install.rake +0 -26
  58. data/public/javascripts/admin/promotions.js +0 -26
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2007-2010, Rails Dog LLC and other contributors
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name Spree nor the names of its contributors may be used to
13
+ endorse or promote products derived from this software without specific
14
+ prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
20
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
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.
@@ -0,0 +1,131 @@
1
+
2
+ var initProductRuleSourceField = function(){
3
+
4
+ $products_source_field = jQuery('.products_rule_products_source_field input');
5
+ $products_source_field.click(function() {
6
+ $rule_container = jQuery(this).parents('.promotion-rule');
7
+ if(this.checked){
8
+ if(this.value == 'manual'){
9
+ $rule_container.find('.products_rule_products').show();
10
+ $rule_container.find('.products_rule_product_group').hide();
11
+ } else {
12
+ $rule_container.find('.products_rule_products').hide();
13
+ $rule_container.find('.products_rule_product_group').show();
14
+ }
15
+ }
16
+ });
17
+ $products_source_field.each(function() {
18
+ $(this).triggerHandler('click');
19
+ });
20
+
21
+ };
22
+
23
+ var initProductActions = function(){
24
+
25
+ $('.calculator-fields').each(function(){
26
+ var $fields_container = $(this);
27
+ var $type_select = $fields_container.find('.type-select');
28
+ var $settings = $fields_container.find('.settings');
29
+ var $warning = $fields_container.find('.warning');
30
+ var originalType = $type_select.val();
31
+
32
+ $warning.hide();
33
+ $type_select.change(function(){
34
+ if( $(this).val() == originalType ){
35
+ $warning.hide();
36
+ $settings.show();
37
+ $settings.find('input').removeAttr('disabled');
38
+ } else {
39
+ $warning.show();
40
+ $settings.hide();
41
+ $settings.find('input').attr('disabled', 'disabled');
42
+ }
43
+ });
44
+ });
45
+
46
+
47
+ //
48
+ // CreateLineItems Promotion Action
49
+ //
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_autocomplete_data,
55
+ formatItem: function(item) {
56
+ return format_autocomplete(item);
57
+ }
58
+ }).result(function(event, data, formatted) {
59
+ if(data){
60
+ // $('#add_product_id').val(data.product.id);
61
+ var url = "/admin/products/" + data.product.permalink + "/variants.json?authenticity_token=" + $('meta[name=csrf-token]').attr("content");
62
+ var $variant_select = $("select[name='add_line_item_variant_id']");
63
+ $variant_select.html('');
64
+ $.getJSON(url, {}, function(variants_data){
65
+ $.each(variants_data, function(){
66
+ $variant_select.append($("<option />").val(this.id).text(this.label));
67
+ });
68
+ });
69
+ }
70
+ }
71
+ );
72
+ }
73
+
74
+ var hideOrShowItemTables = function(){
75
+ $('.promotion_action table').each(function(){
76
+ if($(this).find('td').length == 0){
77
+ $(this).hide();
78
+ } else {
79
+ $(this).show();
80
+ }
81
+ });
82
+ };
83
+ hideOrShowItemTables();
84
+
85
+ // Remove line item
86
+ var setupRemoveLineItems = function(){
87
+ $(".promotion_action.create_line_items table img").unbind('click').click(function(){
88
+ var $container = $(this).parents('.promotion_action');
89
+ var $hiddenField = $container.find("input[type='hidden']");
90
+ var $row = $(this).parents('tr');
91
+ var index = $row.parents('table').find('tr').index($row.get(0));
92
+ // Remove variant_id quantity pair from the string
93
+ var items = _($hiddenField.val().split(',')).compact();
94
+ items.splice(index - 1, 1);
95
+ $hiddenField.val(items.join(','));
96
+ $(this).parents('tr').remove();
97
+ hideOrShowItemTables();
98
+ });
99
+ };
100
+ setupRemoveLineItems();
101
+ // Add line item to list
102
+ $(".promotion_action.create_line_items button.add").click(function(){
103
+ var $container = $(this).parents('.promotion_action');
104
+ var product_name = $container.find("input[name='add_product_name']").val();
105
+ var variant_id = $container.find("select[name='add_line_item_variant_id']").val();
106
+ var variant_name = $container.find("select[name='add_line_item_variant_id'] option:selected").text();
107
+ var quantity = $container.find("input[name='add_quantity']").val();
108
+ if(variant_id){
109
+ // Add to the table
110
+ var newRow = "<tr><td>" + product_name + "</td><td>" + variant_name + "</td><td>" + quantity + "</td><td><img src='/admin/images/icons/cross.png' /></td></tr>";
111
+ $container.find('table').append(newRow);
112
+ // Add to serialized string in hidden text field
113
+ var $hiddenField = $container.find("input[type='hidden']");
114
+ $hiddenField.val($hiddenField.val() + "," + variant_id + "x" + quantity);
115
+ setupRemoveLineItems();
116
+ hideOrShowItemTables();
117
+ }
118
+ return false;
119
+ });
120
+
121
+ } )();
122
+
123
+ }
124
+
125
+ $(document).ready(function() {
126
+ initProductRuleSourceField();
127
+ initProductActions();
128
+ });
129
+
130
+
131
+
@@ -0,0 +1,3 @@
1
+ //= require admin/spree_core
2
+ //= require admin/spree_auth
3
+ //= require_tree .
@@ -0,0 +1,2 @@
1
+ //= require store/spree_core
2
+ //= require store/spree_auth
@@ -1,4 +1,4 @@
1
- .promotion-rule {
1
+ .promotion_rule, .promotion_action {
2
2
  -moz-border-radius: 5px;
3
3
  -webkit-border-radius: 5px;
4
4
  padding: 8px 40px 8px 10px;
@@ -7,48 +7,38 @@
7
7
  margin-bottom: 4px;
8
8
  position: relative;
9
9
  }
10
- .promotion-rule .delete {
10
+ .promotion_rule .delete, .promotion_action .delete {
11
11
  display: block;
12
12
  position: absolute;
13
13
  right: 10px;
14
14
  top: 10px;
15
15
  }
16
-
17
-
18
16
  .products_rule_products_source_field label {
19
17
  display: inline;
20
18
  }
21
-
22
-
23
-
24
19
  .edit_promotion #general_fields, .edit_promotion #expiry_fields {
25
- float: left;
26
- width: 46%;
20
+ float: left;
21
+ width: 46%;
27
22
  }
28
-
29
23
  .edit_promotion #general_fields {
30
- margin-right: 10px;
24
+ margin-right: 10px;
31
25
  }
32
-
33
- .edit_promotion #expiry_fields {
34
-
35
- }
36
-
26
+ .edit_promotion #expiry_fields {}
37
27
  .edit_promotion #starts_at_field, .edit_promotion #expires_at_field {
38
- width: 200px;
39
- float: left;
40
- clear: none;
28
+ width: 200px;
29
+ float: left;
30
+ clear: none;
41
31
  }
42
32
  .edit_promotion #starts_at_field label, .edit_promotion #expires_at_field label{
43
- display: block;
44
- }
45
- #rule_fields {
46
- position: relative;
33
+ display: block;
47
34
  }
48
- #new_product_rule_form {
49
- position: absolute;
50
- text-align:right;
51
- width: 300px;
52
- right: 30px;
53
- bottom: 20px;
35
+ #rule_fields, #action_fields {
36
+ position: relative;
54
37
  }
38
+ #new_product_rule_form, #new_promotion_action_form {
39
+ position: absolute;
40
+ text-align:right;
41
+ width: 300px;
42
+ right: 30px;
43
+ bottom: 20px;
44
+ }
@@ -0,0 +1,5 @@
1
+ /*
2
+ *= require admin/spree_core
3
+ *= require admin/spree_auth
4
+ *= require admin/promotions
5
+ */
@@ -0,0 +1,4 @@
1
+ /*
2
+ *= require store/spree_core
3
+ *= require store/spree_auth
4
+ */
@@ -0,0 +1,28 @@
1
+ class Admin::PromotionActionsController < Admin::BaseController
2
+ def create
3
+ @calculators = Promotion::Actions::CreateAdjustment.calculators
4
+ @promotion = Promotion.find(params[:promotion_id])
5
+ @promotion_action = params[:promotion_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 edit_admin_promotion_path(@promotion)}
12
+ format.js { render :layout => false }
13
+ end
14
+ end
15
+
16
+ def destroy
17
+ @promotion = 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 edit_admin_promotion_path(@promotion)}
24
+ format.js { render :layout => false }
25
+ end
26
+ end
27
+ end
28
+
@@ -10,12 +10,12 @@ class Admin::PromotionsController < Admin::ResourceController
10
10
  end
11
11
  @promotion
12
12
  end
13
-
13
+
14
14
  def location_after_save
15
15
  edit_admin_promotion_url(@promotion)
16
16
  end
17
17
 
18
18
  def load_data
19
- @calculators = Promotion.calculators
19
+ @calculators = Promotion::Actions::CreateAdjustment.calculators
20
20
  end
21
21
  end
@@ -0,0 +1,8 @@
1
+ # Keep a record ot all static page paths visited for promotions that require them
2
+ ContentController.class_eval do
3
+ after_filter :store_visited_path
4
+ def store_visited_path
5
+ session[:visited_paths] ||= []
6
+ session[:visited_paths] = (session[:visited_paths] + [params[:path]]).compact.uniq
7
+ end
8
+ end
@@ -1,10 +1,19 @@
1
1
  OrdersController.class_eval do
2
- after_filter :clear_promotions
3
2
 
4
- private
5
- def clear_promotions
6
- current_order.promotion_credits.destroy_all if current_order
3
+ def update
4
+ @order = current_order
5
+ if @order.update_attributes(params[:order])
6
+
7
+ if @order.coupon_code.present?
8
+ fire_event('spree.checkout.coupon_code_added', :coupon_code => @order.coupon_code)
9
+ end
10
+
11
+ @order.line_items = @order.line_items.select {|li| li.quantity > 0 }
12
+ fire_event('spree.order.contents_changed')
13
+ respond_with(@order) { |format| format.html { redirect_to cart_path } }
14
+ else
15
+ respond_with(@order)
16
+ end
7
17
  end
8
18
 
9
19
  end
10
-
@@ -0,0 +1,3 @@
1
+ Adjustment.class_eval do
2
+ scope :promotion, lambda { where('label LIKE ?', "#{I18n.t(:promotion)}%") }
3
+ end
@@ -0,0 +1,24 @@
1
+ Order.class_eval do
2
+
3
+ attr_accessible :coupon_code
4
+ attr_accessor :coupon_code
5
+
6
+ def promotion_credit_exists?(promotion)
7
+ !! adjustments.promotion.reload.detect { |credit| credit.originator.promotion.id == promotion.id }
8
+ end
9
+
10
+ def products
11
+ line_items.map {|li| li.variant.product}
12
+ end
13
+
14
+ unless self.instance_methods.include?("update_adjustments_with_promotion_limiting")
15
+ def update_adjustments_with_promotion_limiting
16
+ update_adjustments_without_promotion_limiting
17
+ return if adjustments.promotion.eligible.none?
18
+ most_valuable_adjustment = adjustments.promotion.eligible.max{|a,b| a.amount.abs <=> b.amount.abs}
19
+ ( adjustments.promotion.eligible - [most_valuable_adjustment] ).each{|adjustment| adjustment.update_attribute_without_callbacks(:eligible, false)}
20
+ end
21
+ alias_method_chain :update_adjustments, :promotion_limiting
22
+ end
23
+
24
+ end
@@ -0,0 +1,10 @@
1
+ Product.class_eval do
2
+ has_and_belongs_to_many :promotion_rules
3
+
4
+ def possible_promotions
5
+ rules_with_matching_product_groups = product_groups.map(&:promotion_rules).flatten
6
+ all_rules = promotion_rules + rules_with_matching_product_groups
7
+ promotion_ids = all_rules.map(&:activator_id).uniq
8
+ Promotion.advertised.where(:id => promotion_ids)
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ ProductGroup.class_eval do
2
+ has_many :promotion_rules
3
+ end
@@ -1,13 +1,37 @@
1
- class Promotion < ActiveRecord::Base
2
- has_many :promotion_credits, :as => :source
3
- calculated_adjustments
4
- alias credits promotion_credits
1
+ class Promotion < Activator
5
2
 
6
- has_many :promotion_rules, :autosave => true
7
- accepts_nested_attributes_for :promotion_rules
3
+
4
+ MATCH_POLICIES = %w(all any)
5
+
6
+ preference :usage_limit, :integer
7
+ preference :match_policy, :string, :default => MATCH_POLICIES.first
8
+ preference :code, :string
9
+ preference :advertise, :boolean, :default => false
10
+
11
+ [:usage_limit, :match_policy, :code, :advertise].each do |field|
12
+ alias_method field, "preferred_#{field}"
13
+ alias_method "#{field}=", "preferred_#{field}="
14
+ end
15
+
16
+ has_many :promotion_rules, :foreign_key => 'activator_id', :autosave => true, :dependent => :destroy
8
17
  alias_method :rules, :promotion_rules
18
+ accepts_nested_attributes_for :promotion_rules
19
+
20
+ has_many :promotion_actions, :foreign_key => 'activator_id', :autosave => true, :dependent => :destroy
21
+ alias_method :actions, :promotion_actions
22
+ accepts_nested_attributes_for :promotion_actions
23
+
24
+ # TODO: This shouldn't be necessary with :autosave option but nested attribute updating of actions is broken without it
25
+ after_save :save_rules_and_actions
26
+ def save_rules_and_actions
27
+ (rules + actions).each &:save
28
+ end
29
+
9
30
 
10
31
  validates :name, :presence => true
32
+ validates :preferred_code, :presence => true, :if => lambda{|r| r.event_name == 'spree.ceckout.coupon_code_added' }
33
+
34
+ scope :advertised, includes(:stored_preferences).where(:preferences => {:name => 'advertise', :value => '1'})
11
35
 
12
36
  # 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
13
37
  # is provided
@@ -17,52 +41,32 @@ class Promotion < ActiveRecord::Base
17
41
  end
18
42
  end
19
43
 
20
- MATCH_POLICIES = %w(all any)
21
-
22
- scope :automatic, where("code IS NULL OR code = ''")
23
- scope :manual, where("code IS NOT NULL AND code <> ''")
24
-
25
- def eligible?(order)
26
- !expired? && rules_are_eligible?(order)
27
- end
28
44
 
29
- def expired?
30
- starts_at && Time.now < starts_at ||
31
- expires_at && Time.now > expires_at ||
32
- usage_limit && credits_count >= usage_limit
45
+ def activate(payload)
46
+ if eligible?(payload[:order], payload)
47
+ actions.each do |action|
48
+ action.perform(payload)
49
+ end
50
+ end
33
51
  end
34
52
 
35
- def credits_count
36
- credits.with_order.count
53
+ def eligible?(order, options = {})
54
+ !expired? && rules_are_eligible?(order, options) && coupon_is_eligible?(order, options[:coupon_code])
37
55
  end
38
56
 
39
- def rules_are_eligible?(order)
57
+ def rules_are_eligible?(order, options = {})
40
58
  return true if rules.none?
41
59
  if match_policy == 'all'
42
- rules.all?{|r| r.eligible?(order)}
60
+ rules.all?{|r| r.eligible?(order, options)}
43
61
  else
44
- rules.any?{|r| r.eligible?(order)}
45
- end
46
- end
47
-
48
- def create_discount(order)
49
- return if order.promotion_credit_exists?(self)
50
- if eligible?(order) and amount = compute(order)
51
- order.promotion_credits.reload.clear unless combine? and order.promotion_credits.all? { |credit| credit.source.combine? }
52
- order.update!
53
- PromotionCredit.create!({
54
- :label => "#{I18n.t(:coupon)} (#{code})",
55
- :source => self,
56
- :amount => -amount.abs,
57
- :order => order
58
- })
62
+ rules.any?{|r| r.eligible?(order, options)}
59
63
  end
60
64
  end
61
65
 
62
- def compute(order)
63
- amount = calculator.compute(order)
64
- amount = order.item_total if amount > order.item_total
65
- -amount
66
+ def coupon_is_eligible?(order, code = nil)
67
+ return true if order && order.promotion_credit_exists?(self)
68
+ return true if event_name != 'spree.checkout.coupon_code_added'
69
+ code.to_s.strip.downcase == preferred_code.to_s.strip.downcase
66
70
  end
67
71
 
68
72
  # Products assigned to all product rules
@@ -70,4 +74,20 @@ class Promotion < ActiveRecord::Base
70
74
  @products ||= rules.of_type("Promotion::Rules::Product").map(&:products).flatten.uniq
71
75
  end
72
76
 
77
+ def expired?
78
+ super || usage_limit_exceeded?
79
+ end
80
+
81
+ def usage_limit_exceeded?
82
+ preferred_usage_limit.present? && preferred_usage_limit > 0 && credits_count >= preferred_usage_limit
83
+ end
84
+
85
+ def credits
86
+ Adjustment.where(:originator_id => actions.map(&:id))
87
+ end
88
+
89
+ def credits_count
90
+ credits.count
91
+ end
92
+
73
93
  end