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.
Files changed (94) hide show
  1. data/LICENSE +2 -2
  2. data/app/assets/javascripts/admin/promotions.js +3 -23
  3. data/app/controllers/spree/admin/promotion_actions_controller.rb +27 -0
  4. data/app/controllers/spree/admin/promotion_rules_controller.rb +26 -0
  5. data/app/controllers/spree/admin/promotions_controller.rb +24 -0
  6. data/app/controllers/spree/checkout_controller_decorator.rb +39 -0
  7. data/app/controllers/{content_controller_decorator.rb → spree/content_controller_decorator.rb} +4 -2
  8. data/app/controllers/{orders_controller_decorator.rb → spree/orders_controller_decorator.rb} +1 -3
  9. data/app/models/{adjustment_decorator.rb → spree/adjustment_decorator.rb} +1 -1
  10. data/app/models/spree/calculator/free_shipping.rb +18 -0
  11. data/app/models/{order_decorator.rb → spree/order_decorator.rb} +5 -4
  12. data/app/models/{product_decorator.rb → spree/product_decorator.rb} +3 -3
  13. data/app/models/spree/product_group_decorator.rb +3 -0
  14. data/app/models/spree/promotion/actions/create_adjustment.rb +33 -0
  15. data/app/models/spree/promotion/actions/create_line_items.rb +28 -0
  16. data/app/models/spree/promotion/rules/first_order.rb +7 -0
  17. data/app/models/spree/promotion/rules/item_total.rb +14 -0
  18. data/app/models/spree/promotion/rules/landing_page.rb +16 -0
  19. data/app/models/spree/promotion/rules/product.rb +40 -0
  20. data/app/models/spree/promotion/rules/user.rb +18 -0
  21. data/app/models/spree/promotion/rules/user_logged_in.rb +15 -0
  22. data/app/models/spree/promotion.rb +131 -0
  23. data/app/models/spree/promotion_action.rb +17 -0
  24. data/app/models/spree/promotion_action_line_item.rb +6 -0
  25. data/app/models/spree/promotion_rule.rb +12 -0
  26. data/app/overrides/promo_admin_tabs.rb +2 -2
  27. data/app/overrides/promo_coupon_code_field.rb +2 -2
  28. data/app/overrides/promo_product_properties.rb +2 -2
  29. data/app/views/{admin → spree/admin}/promotion_actions/create.js.erb +1 -1
  30. data/app/views/{admin → spree/admin}/promotion_actions/destroy.js.erb +0 -0
  31. data/app/views/{admin → spree/admin}/promotion_rules/create.js.erb +1 -1
  32. data/app/views/{admin → spree/admin}/promotion_rules/destroy.js.erb +0 -0
  33. data/app/views/{admin → spree/admin}/promotions/_actions.html.erb +5 -5
  34. data/app/views/{admin → spree/admin}/promotions/_form.html.erb +11 -11
  35. data/app/views/{admin → spree/admin}/promotions/_promotion_action.html.erb +2 -2
  36. data/app/views/{admin → spree/admin}/promotions/_promotion_rule.html.erb +2 -2
  37. data/app/views/{admin → spree/admin}/promotions/_rules.html.erb +2 -2
  38. data/app/views/{admin → spree/admin}/promotions/_tab.html.erb +0 -0
  39. data/app/views/{admin → spree/admin}/promotions/actions/_create_adjustment.html.erb +1 -1
  40. data/app/views/{admin → spree/admin}/promotions/actions/_create_line_items.html.erb +1 -0
  41. data/app/views/{admin → spree/admin}/promotions/actions/_give_store_credit.html.erb +1 -1
  42. data/app/views/{admin → spree/admin}/promotions/edit.html.erb +3 -3
  43. data/app/views/{admin → spree/admin}/promotions/index.html.erb +2 -2
  44. data/app/views/{admin → spree/admin}/promotions/new.html.erb +1 -1
  45. data/app/views/{admin → spree/admin}/promotions/rules/_first_order.html.erb +0 -0
  46. data/app/views/{admin → spree/admin}/promotions/rules/_item_total.html.erb +1 -1
  47. data/app/views/{admin → spree/admin}/promotions/rules/_landing_page.html.erb +0 -0
  48. data/app/views/{admin → spree/admin}/promotions/rules/_product.html.erb +2 -2
  49. data/app/views/{admin → spree/admin}/promotions/rules/_user.html.erb +0 -0
  50. data/app/views/{admin → spree/admin}/promotions/rules/_user_logged_in.html.erb +0 -0
  51. data/app/views/{checkout → spree/checkout}/_coupon_code_field.html.erb +1 -1
  52. data/app/views/{products → spree/products}/_promotions.html.erb +0 -0
  53. data/config/locales/en.yml +7 -0
  54. data/config/routes.rb +1 -1
  55. data/db/migrate/20100419190933_rename_coupons_to_promotions.rb +4 -4
  56. data/db/migrate/20100426100726_create_promotion_rules.rb +4 -4
  57. data/db/migrate/20100501084722_match_policy_for_promotions.rb +3 -7
  58. data/db/migrate/20100501095202_create_promotion_rules_users.rb +3 -3
  59. data/db/migrate/20100502163939_name_for_promotions.rb +3 -7
  60. data/db/migrate/20100923095305_update_calculable_type_for_promotions.rb +4 -4
  61. data/db/migrate/20101026184833_migrate_adjustments.rb +4 -4
  62. data/db/migrate/20110331094351_promotion_changes_to_subclass_of_activator.rb +13 -13
  63. data/db/migrate/20110404123358_create_promotion_actions.rb +2 -6
  64. data/db/migrate/20110601202923_create_promotion_action_line_items.rb +4 -6
  65. data/db/migrate/20111013155037_namespace_promo_tables.rb +9 -0
  66. data/lib/spree/promo/engine.rb +59 -0
  67. data/lib/spree/promo/environment.rb +1 -1
  68. data/lib/spree/promo.rb +10 -0
  69. data/lib/spree_promo.rb +1 -71
  70. metadata +77 -72
  71. data/app/controllers/admin/promotion_actions_controller.rb +0 -28
  72. data/app/controllers/admin/promotion_rules_controller.rb +0 -26
  73. data/app/controllers/admin/promotions_controller.rb +0 -21
  74. data/app/controllers/checkout_controller_decorator.rb +0 -13
  75. data/app/models/calculator/free_shipping.rb +0 -19
  76. data/app/models/pending_promotion.rb +0 -4
  77. data/app/models/product_group_decorator.rb +0 -3
  78. data/app/models/promotion/actions/create_adjustment.rb +0 -28
  79. data/app/models/promotion/actions/create_line_items.rb +0 -29
  80. data/app/models/promotion/rules/first_order.rb +0 -7
  81. data/app/models/promotion/rules/item_total.rb +0 -13
  82. data/app/models/promotion/rules/landing_page.rb +0 -17
  83. data/app/models/promotion/rules/product.rb +0 -40
  84. data/app/models/promotion/rules/user.rb +0 -17
  85. data/app/models/promotion/rules/user_logged_in.rb +0 -13
  86. data/app/models/promotion.rb +0 -97
  87. data/app/models/promotion_action.rb +0 -17
  88. data/app/models/promotion_action_line_item.rb +0 -6
  89. data/app/models/promotion_rule.rb +0 -11
  90. data/app/models/user_decorator.rb +0 -4
  91. data/config/cucumber.yml +0 -10
  92. data/db/migrate/20120119024708_create_pending_promotions.rb +0 -11
  93. data/lib/tasks/promotions.rake +0 -11
  94. data/lib/tasks/promotions_extension_tasks.rake +0 -17
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2007-2011, Rails Dog LLC and other contributors
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('.promotion_rule');
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='add_line_item_variant_id']").val();
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
@@ -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
- session[:visited_paths] = (session[:visited_paths] + [params[:path]]).compact.uniq
7
+ path = "content/#{params[:action]}"
8
+ session[:visited_paths] = (session[:visited_paths] << path).compact.uniq
7
9
  end
8
10
  end
@@ -1,5 +1,4 @@
1
- OrdersController.class_eval do
2
-
1
+ Spree::OrdersController.class_eval do
3
2
  def update
4
3
  @order = current_order
5
4
  if @order.update_attributes(params[:order])
@@ -15,5 +14,4 @@ OrdersController.class_eval do
15
14
  respond_with(@order)
16
15
  end
17
16
  end
18
-
19
17
  end
@@ -1,3 +1,3 @@
1
- Adjustment.class_eval do
1
+ Spree::Adjustment.class_eval do
2
2
  scope :promotion, lambda { where('label LIKE ?', "#{I18n.t(:promotion)}%") }
3
3
  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.instance_methods.include?("update_adjustments_with_promotion_limiting")
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,3 @@
1
+ Spree::ProductGroup.class_eval do
2
+ has_many :promotion_rules
3
+ 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,7 @@
1
+ module Spree
2
+ class Promotion::Rules::FirstOrder < PromotionRule
3
+ def eligible?(order, options = {})
4
+ order.user && order.user.orders.complete.count == 0
5
+ end
6
+ end
7
+ 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,6 @@
1
+ module Spree
2
+ class PromotionActionLineItem < ActiveRecord::Base
3
+ belongs_to :promotion_action, :class_name => 'Spree::Promotion::Actions::CreateLineItems'
4
+ belongs_to :variant
5
+ end
6
+ 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)