dup_spree_promo 1.3.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +26 -0
- data/app/assets/javascripts/admin/promotions.js +98 -0
- data/app/assets/javascripts/admin/spree_promo.js +32 -0
- data/app/assets/javascripts/store/spree_promo.js +1 -0
- data/app/assets/stylesheets/admin/spree_promo.css +3 -0
- data/app/assets/stylesheets/store/spree_promo.css +17 -0
- data/app/controllers/spree/admin/promotion_actions_controller.rb +27 -0
- data/app/controllers/spree/admin/promotion_rules_controller.rb +32 -0
- data/app/controllers/spree/admin/promotions_controller.rb +30 -0
- data/app/controllers/spree/checkout_controller_decorator.rb +30 -0
- data/app/controllers/spree/content_controller_decorator.rb +13 -0
- data/app/controllers/spree/orders_controller_decorator.rb +24 -0
- data/app/controllers/spree/store_controller_decorator.rb +55 -0
- data/app/helpers/spree/promotion_rules_helper.rb +13 -0
- data/app/models/spree/adjustment_decorator.rb +7 -0
- data/app/models/spree/calculator/free_shipping.rb +18 -0
- data/app/models/spree/calculator/percent_per_item.rb +48 -0
- data/app/models/spree/order_decorator.rb +19 -0
- data/app/models/spree/order_updater_decorator.rb +14 -0
- data/app/models/spree/payment_decorator.rb +5 -0
- data/app/models/spree/product_decorator.rb +8 -0
- data/app/models/spree/promotion.rb +105 -0
- data/app/models/spree/promotion/actions/create_adjustment.rb +48 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +23 -0
- data/app/models/spree/promotion/rules/first_order.rb +12 -0
- data/app/models/spree/promotion/rules/item_total.rb +21 -0
- data/app/models/spree/promotion/rules/product.rb +46 -0
- data/app/models/spree/promotion/rules/user.rb +24 -0
- data/app/models/spree/promotion/rules/user_logged_in.rb +20 -0
- data/app/models/spree/promotion_action.rb +19 -0
- data/app/models/spree/promotion_action_line_item.rb +8 -0
- data/app/models/spree/promotion_rule.rb +25 -0
- data/app/overrides/promo_admin_tabs.rb +6 -0
- data/app/overrides/promo_cart_coupon_code_field.rb +6 -0
- data/app/overrides/promo_coupon_code_field.rb +6 -0
- data/app/overrides/promo_product_properties.rb +6 -0
- data/app/views/spree/admin/promotion_actions/create.js.erb +12 -0
- data/app/views/spree/admin/promotion_actions/destroy.js.erb +1 -0
- data/app/views/spree/admin/promotion_rules/create.js.erb +13 -0
- data/app/views/spree/admin/promotion_rules/destroy.js.erb +3 -0
- data/app/views/spree/admin/promotions/_actions.html.erb +32 -0
- data/app/views/spree/admin/promotions/_form.html.erb +56 -0
- data/app/views/spree/admin/promotions/_promotion_action.html.erb +11 -0
- data/app/views/spree/admin/promotions/_promotion_rule.html.erb +9 -0
- data/app/views/spree/admin/promotions/_rules.html.erb +45 -0
- data/app/views/spree/admin/promotions/_tab.html.erb +1 -0
- data/app/views/spree/admin/promotions/actions/_create_adjustment.html.erb +26 -0
- data/app/views/spree/admin/promotions/actions/_create_line_items.html.erb +22 -0
- data/app/views/spree/admin/promotions/edit.html.erb +28 -0
- data/app/views/spree/admin/promotions/index.html.erb +52 -0
- data/app/views/spree/admin/promotions/new.html.erb +16 -0
- data/app/views/spree/admin/promotions/rules/_first_order.html.erb +0 -0
- data/app/views/spree/admin/promotions/rules/_item_total.html.erb +6 -0
- data/app/views/spree/admin/promotions/rules/_landing_page.html.erb +5 -0
- data/app/views/spree/admin/promotions/rules/_product.html.erb +9 -0
- data/app/views/spree/admin/promotions/rules/_user.html.erb +4 -0
- data/app/views/spree/admin/promotions/rules/_user_logged_in.html.erb +0 -0
- data/app/views/spree/checkout/_coupon_code_field.html.erb +6 -0
- data/app/views/spree/orders/_coupon_code_field.html.erb +7 -0
- data/app/views/spree/products/_promotions.html.erb +23 -0
- data/config/locales/en.yml +98 -0
- data/config/routes.rb +8 -0
- data/db/migrate/20120831092359_spree_promo_one_two.rb +45 -0
- data/lib/spree/promo.rb +9 -0
- data/lib/spree/promo/engine.rb +56 -0
- data/lib/spree/promo/environment.rb +9 -0
- data/lib/spree_promo.rb +1 -0
- metadata +129 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
module Spree
|
2
|
+
|
3
|
+
# A calculator for promotions that calculates a percent-off discount
|
4
|
+
# for all matching products in an order. This should not be used as a
|
5
|
+
# shipping calculator since it would be the same thing as a flat percent
|
6
|
+
# off the entire order.
|
7
|
+
|
8
|
+
class Calculator::PercentPerItem < Calculator
|
9
|
+
preference :percent, :decimal, :default => 0
|
10
|
+
|
11
|
+
attr_accessible :preferred_percent
|
12
|
+
|
13
|
+
def self.description
|
14
|
+
I18n.t(:percent_per_item)
|
15
|
+
end
|
16
|
+
|
17
|
+
def compute(object=nil)
|
18
|
+
return 0 if object.nil?
|
19
|
+
object.line_items.reduce(0) do |sum, line_item|
|
20
|
+
sum += value_for_line_item(line_item)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Returns all products that match the promotion's rule.
|
27
|
+
def matching_products
|
28
|
+
@matching_products ||= if compute_on_promotion?
|
29
|
+
self.calculable.promotion.rules.map(&:products).flatten
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Calculates the discount value of each line item. Returns zero
|
34
|
+
# unless the product is included in the promotion rules.
|
35
|
+
def value_for_line_item(line_item)
|
36
|
+
if compute_on_promotion?
|
37
|
+
return 0 unless matching_products.include?(line_item.product)
|
38
|
+
end
|
39
|
+
line_item.price * line_item.quantity * preferred_percent
|
40
|
+
end
|
41
|
+
|
42
|
+
# Determines wether or not the calculable object is a promotion
|
43
|
+
def compute_on_promotion?
|
44
|
+
@compute_on_promotion ||= self.calculable.respond_to?(:promotion)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Spree::Order.class_eval do
|
2
|
+
attr_accessible :coupon_code
|
3
|
+
attr_reader :coupon_code
|
4
|
+
|
5
|
+
def coupon_code=(code)
|
6
|
+
@coupon_code = code.strip.downcase rescue nil
|
7
|
+
end
|
8
|
+
|
9
|
+
# Tells us if there if the specified promotion is already associated with the order
|
10
|
+
# regardless of whether or not its currently eligible. Useful because generally
|
11
|
+
# you would only want a promotion to apply to order no more than once.
|
12
|
+
def promotion_credit_exists?(promotion)
|
13
|
+
!! adjustments.promotion.reload.detect { |credit| credit.originator.promotion.id == promotion.id }
|
14
|
+
end
|
15
|
+
|
16
|
+
def promo_total
|
17
|
+
adjustments.eligible.promotion.map(&:amount).sum
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Spree::OrderUpdater.class_eval do
|
2
|
+
unless self.method_defined?('update_adjustments_with_promotion_limiting')
|
3
|
+
def update_adjustments_with_promotion_limiting
|
4
|
+
update_adjustments_without_promotion_limiting
|
5
|
+
return if adjustments.promotion.eligible.none?
|
6
|
+
most_valuable_adjustment = adjustments.promotion.eligible.max{|a,b| a.amount.abs <=> b.amount.abs}
|
7
|
+
current_adjustments = (adjustments.promotion.eligible - [most_valuable_adjustment])
|
8
|
+
current_adjustments.each do |adjustment|
|
9
|
+
adjustment.update_attribute_without_callbacks(:eligible, false)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
alias_method_chain :update_adjustments, :promotion_limiting
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
Spree::Product.class_eval do
|
2
|
+
has_and_belongs_to_many :promotion_rules, :join_table => :spree_products_promotion_rules
|
3
|
+
|
4
|
+
def possible_promotions
|
5
|
+
promotion_ids = promotion_rules.map(&:activator_id).uniq
|
6
|
+
Spree::Promotion.advertised.where(:id => promotion_ids).reject(&:expired?)
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Spree
|
2
|
+
class Promotion < Spree::Activator
|
3
|
+
MATCH_POLICIES = %w(all any)
|
4
|
+
UNACTIVATABLE_ORDER_STATES = ["complete", "awaiting_return", "returned"]
|
5
|
+
|
6
|
+
Activator.event_names << 'spree.checkout.coupon_code_added'
|
7
|
+
Activator.event_names << 'spree.content.visited'
|
8
|
+
|
9
|
+
has_many :promotion_rules, :foreign_key => :activator_id, :autosave => true, :dependent => :destroy
|
10
|
+
alias_method :rules, :promotion_rules
|
11
|
+
accepts_nested_attributes_for :promotion_rules
|
12
|
+
|
13
|
+
has_many :promotion_actions, :foreign_key => :activator_id, :autosave => true, :dependent => :destroy
|
14
|
+
alias_method :actions, :promotion_actions
|
15
|
+
accepts_nested_attributes_for :promotion_actions
|
16
|
+
|
17
|
+
validates_associated :rules
|
18
|
+
|
19
|
+
attr_accessible :name, :event_name, :code, :match_policy,
|
20
|
+
:path, :advertise, :description, :usage_limit,
|
21
|
+
:starts_at, :expires_at, :promotion_rules_attributes,
|
22
|
+
:promotion_actions_attributes
|
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
|
+
|
30
|
+
validates :name, :presence => true
|
31
|
+
validates :code, :presence => true, :if => lambda{|r| r.event_name == 'spree.checkout.coupon_code_added' }
|
32
|
+
validates :path, :presence => true, :if => lambda{|r| r.event_name == 'spree.content.visited' }
|
33
|
+
validates :usage_limit, :numericality => { :greater_than => 0, :allow_nil => true }
|
34
|
+
|
35
|
+
def self.advertised
|
36
|
+
where(:advertise => true)
|
37
|
+
end
|
38
|
+
|
39
|
+
def activate(payload)
|
40
|
+
return unless order_activatable? payload[:order]
|
41
|
+
|
42
|
+
if code.present?
|
43
|
+
event_code = payload[:coupon_code]
|
44
|
+
return unless event_code == self.code
|
45
|
+
end
|
46
|
+
|
47
|
+
if path.present?
|
48
|
+
return unless path == payload[:path]
|
49
|
+
end
|
50
|
+
|
51
|
+
actions.each do |action|
|
52
|
+
action.perform(payload)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# called anytime order.update! happens
|
57
|
+
def eligible?(order)
|
58
|
+
return false if expired? || usage_limit_exceeded?(order)
|
59
|
+
rules_are_eligible?(order, {})
|
60
|
+
end
|
61
|
+
|
62
|
+
def rules_are_eligible?(order, options = {})
|
63
|
+
return true if rules.none?
|
64
|
+
eligible = lambda { |r| r.eligible?(order, options) }
|
65
|
+
if match_policy == 'all'
|
66
|
+
rules.all?(&eligible)
|
67
|
+
else
|
68
|
+
rules.any?(&eligible)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def order_activatable?(order)
|
73
|
+
order &&
|
74
|
+
created_at.to_i < order.created_at.to_i &&
|
75
|
+
!UNACTIVATABLE_ORDER_STATES.include?(order.state)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Products assigned to all product rules
|
79
|
+
def products
|
80
|
+
@products ||= rules.of_type('Spree::Promotion::Rules::Product').map(&:products).flatten.uniq
|
81
|
+
end
|
82
|
+
|
83
|
+
def usage_limit_exceeded?(order = nil)
|
84
|
+
usage_limit.present? && usage_limit > 0 && adjusted_credits_count(order) >= usage_limit
|
85
|
+
end
|
86
|
+
|
87
|
+
def adjusted_credits_count(order)
|
88
|
+
return credits_count if order.nil?
|
89
|
+
credits_count - (order.promotion_credit_exists?(self) ? 1 : 0)
|
90
|
+
end
|
91
|
+
|
92
|
+
def credits
|
93
|
+
Adjustment.promotion.where(:originator_id => actions.map(&:id))
|
94
|
+
end
|
95
|
+
|
96
|
+
def credits_count
|
97
|
+
credits.count
|
98
|
+
end
|
99
|
+
|
100
|
+
def code=(coupon_code)
|
101
|
+
write_attribute(:code, (coupon_code.downcase.strip rescue nil))
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Spree
|
2
|
+
class Promotion
|
3
|
+
module Actions
|
4
|
+
class CreateAdjustment < PromotionAction
|
5
|
+
calculated_adjustments
|
6
|
+
|
7
|
+
delegate :eligible?, :to => :promotion
|
8
|
+
|
9
|
+
before_validation :ensure_action_has_calculator
|
10
|
+
|
11
|
+
def perform(options = {})
|
12
|
+
return unless order = options[:order]
|
13
|
+
# Nothing to do if the promotion is already associated with the order
|
14
|
+
return if order.promotion_credit_exists?(promotion)
|
15
|
+
|
16
|
+
order.adjustments.promotion.reload.clear
|
17
|
+
order.update!
|
18
|
+
create_adjustment("#{I18n.t(:promotion)} (#{promotion.name})", order, order)
|
19
|
+
order.update!
|
20
|
+
end
|
21
|
+
|
22
|
+
# override of CalculatedAdjustments#create_adjustment so promotional
|
23
|
+
# adjustments are added all the time. They will get their eligability
|
24
|
+
# set to false if the amount is 0
|
25
|
+
def create_adjustment(label, target, calculable, mandatory=false)
|
26
|
+
amount = compute_amount(calculable)
|
27
|
+
params = { :amount => amount,
|
28
|
+
:source => calculable,
|
29
|
+
:originator => self,
|
30
|
+
:label => label,
|
31
|
+
:mandatory => mandatory }
|
32
|
+
target.adjustments.create(params, :without_protection => true)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Ensure a negative amount which does not exceed the sum of the order's item_total and ship_total
|
36
|
+
def compute_amount(calculable)
|
37
|
+
[(calculable.item_total + calculable.ship_total), super.to_f.abs].min * -1
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def ensure_action_has_calculator
|
42
|
+
return if self.calculator
|
43
|
+
self.calculator = Calculator::FlatPercentItemTotal.new
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Spree
|
2
|
+
class Promotion
|
3
|
+
module Actions
|
4
|
+
class CreateLineItems < PromotionAction
|
5
|
+
has_many :promotion_action_line_items, :foreign_key => :promotion_action_id
|
6
|
+
accepts_nested_attributes_for :promotion_action_line_items
|
7
|
+
attr_accessible :promotion_action_line_items_attributes
|
8
|
+
|
9
|
+
|
10
|
+
def perform(options = {})
|
11
|
+
return unless order = options[:order]
|
12
|
+
promotion_action_line_items.each do |item|
|
13
|
+
current_quantity = order.quantity_of(item.variant)
|
14
|
+
if current_quantity < item.quantity
|
15
|
+
order.add_variant(item.variant, item.quantity - current_quantity)
|
16
|
+
order.update!
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# A rule to apply to an order greater than (or greater than or equal to)
|
2
|
+
# a specific amount
|
3
|
+
module Spree
|
4
|
+
class Promotion
|
5
|
+
module Rules
|
6
|
+
class ItemTotal < PromotionRule
|
7
|
+
preference :amount, :decimal, :default => 100.00
|
8
|
+
preference :operator, :string, :default => '>'
|
9
|
+
|
10
|
+
attr_accessible :preferred_amount, :preferred_operator
|
11
|
+
|
12
|
+
OPERATORS = ['gt', 'gte']
|
13
|
+
|
14
|
+
def eligible?(order, options = {})
|
15
|
+
item_total = order.line_items.map(&:amount).sum
|
16
|
+
item_total.send(preferred_operator == 'gte' ? :>= : :>, BigDecimal.new(preferred_amount.to_s))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,46 @@
|
|
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
|
6
|
+
module Rules
|
7
|
+
class Product < PromotionRule
|
8
|
+
has_and_belongs_to_many :products, :class_name => '::Spree::Product', :join_table => 'spree_products_promotion_rules', :foreign_key => 'promotion_rule_id'
|
9
|
+
validate :only_one_promotion_per_product
|
10
|
+
|
11
|
+
MATCH_POLICIES = %w(any all)
|
12
|
+
preference :match_policy, :string, :default => MATCH_POLICIES.first
|
13
|
+
|
14
|
+
# scope/association that is used to test eligibility
|
15
|
+
def eligible_products
|
16
|
+
products
|
17
|
+
end
|
18
|
+
|
19
|
+
def eligible?(order, options = {})
|
20
|
+
return true if eligible_products.empty?
|
21
|
+
if preferred_match_policy == 'all'
|
22
|
+
eligible_products.all? {|p| order.products.include?(p) }
|
23
|
+
else
|
24
|
+
order.products.any? {|p| eligible_products.include?(p) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def product_ids_string
|
29
|
+
product_ids.join(',')
|
30
|
+
end
|
31
|
+
|
32
|
+
def product_ids_string=(s)
|
33
|
+
self.product_ids = s.to_s.split(',').map(&:strip)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def only_one_promotion_per_product
|
39
|
+
if Spree::Promotion::Rules::Product.all.map(&:products).flatten.uniq!
|
40
|
+
errors[:base] << "You can't create two promotions for the same product"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Spree
|
2
|
+
class Promotion
|
3
|
+
module Rules
|
4
|
+
class User < PromotionRule
|
5
|
+
attr_accessible :user_ids_string
|
6
|
+
|
7
|
+
belongs_to :user, :class_name => Spree.user_class.to_s
|
8
|
+
has_and_belongs_to_many :users, :class_name => Spree.user_class.to_s, :join_table => 'spree_promotion_rules_users', :foreign_key => 'promotion_rule_id'
|
9
|
+
|
10
|
+
def eligible?(order, options = {})
|
11
|
+
users.none? or users.include?(order.user)
|
12
|
+
end
|
13
|
+
|
14
|
+
def user_ids_string
|
15
|
+
user_ids.join(',')
|
16
|
+
end
|
17
|
+
|
18
|
+
def user_ids_string=(s)
|
19
|
+
self.user_ids = s.to_s.split(',').map(&:strip)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Spree
|
2
|
+
class Promotion
|
3
|
+
module Rules
|
4
|
+
class UserLoggedIn < PromotionRule
|
5
|
+
|
6
|
+
def eligible?(order, options = {})
|
7
|
+
# this is tricky. We couldn't use any of the devise methods since we aren't in the controller.
|
8
|
+
# we need to rely on the controller already having done this for us.
|
9
|
+
|
10
|
+
# The thinking is that the controller should have some sense of what state
|
11
|
+
# we should be in before firing events,
|
12
|
+
# so the controller will have to set this field.
|
13
|
+
|
14
|
+
return options && options[:user_signed_in]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
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', :class_name => "Spree::Promotion"
|
6
|
+
|
7
|
+
scope :of_type, lambda {|t| {:conditions => {:type => t}}}
|
8
|
+
|
9
|
+
attr_accessible :line_items_string
|
10
|
+
|
11
|
+
# This method should be overriden in subclass
|
12
|
+
# Updates the state of the order or performs some other action depending on the subclass
|
13
|
+
# options will contain the payload from the event that activated the promotion. This will include
|
14
|
+
# the key :user which allows user based actions to be performed in addition to actions on the order
|
15
|
+
def perform(options = {})
|
16
|
+
raise 'perform should be implemented in a sub-class of PromotionAction'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module Spree
|
2
|
+
class PromotionActionLineItem < ActiveRecord::Base
|
3
|
+
belongs_to :promotion_action, :class_name => 'Spree::Promotion::Actions::CreateLineItems'
|
4
|
+
belongs_to :variant, :class_name => "Spree::Variant"
|
5
|
+
|
6
|
+
attr_accessible :quantity, :variant_id
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Base class for all promotion rules
|
2
|
+
module Spree
|
3
|
+
class PromotionRule < ActiveRecord::Base
|
4
|
+
belongs_to :promotion, :foreign_key => 'activator_id', :class_name => "Spree::Promotion"
|
5
|
+
|
6
|
+
scope :of_type, lambda {|t| {:conditions => {:type => t}}}
|
7
|
+
|
8
|
+
validate :promotion, :presence => true
|
9
|
+
validate :unique_per_activator, :on => :create
|
10
|
+
|
11
|
+
attr_accessible :preferred_operator, :preferred_amount, :product, :product_ids_string, :preferred_match_policy
|
12
|
+
|
13
|
+
def eligible?(order, options = {})
|
14
|
+
raise 'eligible? should be implemented in a sub-class of Promotion::PromotionRule'
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def unique_per_activator
|
19
|
+
if Spree::PromotionRule.exists?(:activator_id => activator_id, :type => self.class.name)
|
20
|
+
errors[:base] << "Promotion already contains this rule type"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|