spree_promo 1.1.1 → 1.1.2.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/app/assets/javascripts/admin/promotions.js +5 -9
- data/app/models/spree/order_decorator.rb +6 -1
- data/app/models/spree/promotion/actions/create_adjustment.rb +38 -33
- data/app/models/spree/promotion/actions/create_line_items.rb +26 -21
- data/app/models/spree/promotion/rules/first_order.rb +8 -4
- data/app/models/spree/promotion/rules/item_total.rb +12 -8
- data/app/models/spree/promotion/rules/product.rb +34 -21
- data/app/models/spree/promotion/rules/user.rb +17 -11
- data/app/models/spree/promotion/rules/user_logged_in.rb +13 -8
- data/app/models/spree/promotion.rb +2 -0
- data/app/models/spree/promotion_action.rb +3 -1
- data/app/models/spree/promotion_action_line_item.rb +1 -1
- data/app/models/spree/promotion_rule.rb +1 -1
- data/app/views/spree/admin/promotions/actions/_create_line_items.html.erb +1 -1
- metadata +11 -14
|
@@ -40,19 +40,15 @@ var initProductActions = function(){
|
|
|
40
40
|
|
|
41
41
|
// Remove line item
|
|
42
42
|
var setupRemoveLineItems = function(){
|
|
43
|
-
$(".
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
var index = $row.parents('table').find('tr').index($row.get(0));
|
|
48
|
-
// Remove variant_id quantity pair from the string
|
|
49
|
-
var items = _($hiddenField.val().split(',')).compact();
|
|
50
|
-
items.splice(index - 1, 1);
|
|
51
|
-
$hiddenField.val(items.join(','));
|
|
43
|
+
$(".remove_promotion_line_item").click(function(){
|
|
44
|
+
line_items_el = $($('.line_items_string')[0])
|
|
45
|
+
finder = RegExp($(this).data("variant-id") + "x\\d+")
|
|
46
|
+
line_items_el.val(line_items_el.val().replace(finder, ""))
|
|
52
47
|
$(this).parents('tr').remove();
|
|
53
48
|
hideOrShowItemTables();
|
|
54
49
|
});
|
|
55
50
|
};
|
|
51
|
+
|
|
56
52
|
setupRemoveLineItems();
|
|
57
53
|
// Add line item to list
|
|
58
54
|
$(".promotion_action.create_line_items button.add").unbind('click').click(function(e){
|
|
@@ -18,7 +18,12 @@ Spree::Order.class_eval do
|
|
|
18
18
|
update_adjustments_without_promotion_limiting
|
|
19
19
|
return if adjustments.promotion.eligible.none?
|
|
20
20
|
most_valuable_adjustment = adjustments.promotion.eligible.max{|a,b| a.amount.abs <=> b.amount.abs}
|
|
21
|
-
(
|
|
21
|
+
current_adjustments = (adjustments.promotion.eligible - [most_valuable_adjustment])
|
|
22
|
+
current_adjustments.each do |adjustment|
|
|
23
|
+
if adjustment.originator.calculator.is_a?(Spree::Calculator::PerItem)
|
|
24
|
+
adjustment.update_attribute_without_callbacks(:eligible, false)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
22
27
|
end
|
|
23
28
|
alias_method_chain :update_adjustments, :promotion_limiting
|
|
24
29
|
end
|
|
@@ -1,43 +1,48 @@
|
|
|
1
1
|
module Spree
|
|
2
|
-
class Promotion
|
|
3
|
-
|
|
2
|
+
class Promotion
|
|
3
|
+
module Actions
|
|
4
|
+
class CreateAdjustment < PromotionAction
|
|
5
|
+
calculated_adjustments
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
delegate :eligible?, :to => :promotion
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
before_validation :ensure_action_has_calculator
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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)
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
order.adjustments.promotion.reload.clear
|
|
17
|
+
order.update!
|
|
18
|
+
create_adjustment("#{I18n.t(:promotion)} (#{promotion.name})", order, order)
|
|
19
|
+
order.update!
|
|
20
|
+
end
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
private
|
|
41
|
+
def ensure_action_has_calculator
|
|
42
|
+
return if self.calculator
|
|
43
|
+
self.calculator = Calculator::FlatPercentItemTotal.new
|
|
44
|
+
end
|
|
45
|
+
end
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
|
-
end
|
|
48
|
+
end
|
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
module Spree
|
|
2
|
-
class Promotion
|
|
3
|
-
|
|
2
|
+
class Promotion
|
|
3
|
+
module Actions
|
|
4
|
+
class CreateLineItems < PromotionAction
|
|
5
|
+
has_many :promotion_action_line_items, :foreign_key => 'promotion_action_id'
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
attr_accessor :line_items_string
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
def perform(options = {})
|
|
10
|
+
return unless order = options[:order]
|
|
11
|
+
promotion_action_line_items.each do |item|
|
|
12
|
+
current_quantity = order.quantity_of(item.variant)
|
|
13
|
+
if current_quantity < item.quantity
|
|
14
|
+
order.add_variant(item.variant, item.quantity - current_quantity)
|
|
15
|
+
order.update!
|
|
16
|
+
end
|
|
17
|
+
end
|
|
13
18
|
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
def line_items_string
|
|
21
|
+
promotion_action_line_items.map { |li| "#{li.variant_id}x#{li.quantity}" }.join(',')
|
|
22
|
+
end
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
def line_items_string=(value)
|
|
25
|
+
promotion_action_line_items.destroy_all
|
|
26
|
+
value.to_s.split(',').each do |str|
|
|
27
|
+
variant_id, quantity = str.split('x')
|
|
28
|
+
if variant_id && quantity && variant = Variant.find_by_id(variant_id)
|
|
29
|
+
promotion_action_line_items.create({:variant => variant, :quantity => quantity.to_i}, :without_protection => true)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
27
32
|
end
|
|
28
33
|
end
|
|
29
34
|
end
|
|
30
35
|
end
|
|
31
|
-
end
|
|
36
|
+
end
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
module Spree
|
|
2
|
-
class Promotion
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
class Promotion
|
|
3
|
+
module Rules
|
|
4
|
+
class FirstOrder < PromotionRule
|
|
5
|
+
def eligible?(order, options = {})
|
|
6
|
+
user = order.try(:user) || options[:user]
|
|
7
|
+
!!(user && user.orders.complete.count == 0)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
6
10
|
end
|
|
7
11
|
end
|
|
8
12
|
end
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
# A rule to limit a promotion to a specific user.
|
|
2
2
|
module Spree
|
|
3
|
-
class Promotion
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
class Promotion
|
|
4
|
+
module Rules
|
|
5
|
+
class ItemTotal < PromotionRule
|
|
6
|
+
preference :amount, :decimal, :default => 100.00
|
|
7
|
+
preference :operator, :string, :default => '>'
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
attr_accessible :preferred_amount, :preferred_operator
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
OPERATORS = ['gt', 'gte']
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
def eligible?(order, options = {})
|
|
14
|
+
item_total = order.line_items.map(&:amount).sum
|
|
15
|
+
item_total.send(preferred_operator == 'gte' ? :>= : :>, BigDecimal.new(preferred_amount.to_s))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
14
18
|
end
|
|
15
19
|
end
|
|
16
20
|
end
|
|
@@ -2,32 +2,45 @@
|
|
|
2
2
|
# Can require all or any of the products to be present.
|
|
3
3
|
# Valid products either come from assigned product group or are assingned directly to the rule.
|
|
4
4
|
module Spree
|
|
5
|
-
class Promotion
|
|
6
|
-
|
|
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
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
MATCH_POLICIES = %w(any all)
|
|
12
|
+
preference :match_policy, :string, :default => MATCH_POLICIES.first
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
# scope/association that is used to test eligibility
|
|
15
|
+
def eligible_products
|
|
16
|
+
products
|
|
17
|
+
end
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
def product_ids_string
|
|
29
|
+
product_ids.join(',')
|
|
30
|
+
end
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
31
44
|
end
|
|
32
45
|
end
|
|
33
46
|
end
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
module Spree
|
|
2
|
-
class Promotion
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
class Promotion
|
|
3
|
+
module Rules
|
|
4
|
+
class User < PromotionRule
|
|
5
|
+
attr_accessible :user_ids_string
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
end
|
|
7
|
+
belongs_to :user, :class_name => "Spree::User"
|
|
8
|
+
has_and_belongs_to_many :users, :class_name => '::Spree::User', :join_table => 'spree_promotion_rules_users', :foreign_key => 'promotion_rule_id'
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
def user_ids_string=(s)
|
|
19
|
+
self.user_ids = s.to_s.split(',').map(&:strip)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
16
22
|
end
|
|
17
23
|
end
|
|
18
24
|
end
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
module Spree
|
|
2
|
-
class Promotion
|
|
3
|
-
|
|
2
|
+
class Promotion
|
|
3
|
+
module Rules
|
|
4
|
+
class UserLoggedIn < PromotionRule
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
return options && options[:user_signed_in]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
end
|
|
13
18
|
end
|
|
14
19
|
end
|
|
15
20
|
end
|
|
@@ -14,6 +14,8 @@ module Spree
|
|
|
14
14
|
alias_method :actions, :promotion_actions
|
|
15
15
|
accepts_nested_attributes_for :promotion_actions
|
|
16
16
|
|
|
17
|
+
validates_associated :rules
|
|
18
|
+
|
|
17
19
|
attr_accessible :name, :event_name, :code, :match_policy,
|
|
18
20
|
:path, :advertise, :description, :usage_limit,
|
|
19
21
|
:starts_at, :expires_at, :promotion_rules_attributes,
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
# PromotionActions perform the necessary tasks when a promotion is activated by an event and determined to be eligible.
|
|
3
3
|
module Spree
|
|
4
4
|
class PromotionAction < ActiveRecord::Base
|
|
5
|
-
belongs_to :promotion, :foreign_key => 'activator_id'
|
|
5
|
+
belongs_to :promotion, :foreign_key => 'activator_id', :class_name => "Spree::Promotion"
|
|
6
6
|
|
|
7
7
|
scope :of_type, lambda {|t| {:conditions => {:type => t}}}
|
|
8
8
|
|
|
9
|
+
attr_accessible :line_items_string
|
|
10
|
+
|
|
9
11
|
# This method should be overriden in subclass
|
|
10
12
|
# Updates the state of the order or performs some other action depending on the subclass
|
|
11
13
|
# options will contain the payload from the event that activated the promotion. This will include
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Base class for all promotion rules
|
|
2
2
|
module Spree
|
|
3
3
|
class PromotionRule < ActiveRecord::Base
|
|
4
|
-
belongs_to :promotion, :foreign_key => 'activator_id'
|
|
4
|
+
belongs_to :promotion, :foreign_key => 'activator_id', :class_name => "Spree::Promotion"
|
|
5
5
|
|
|
6
6
|
scope :of_type, lambda {|t| {:conditions => {:type => t}}}
|
|
7
7
|
|
metadata
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_promo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
5
|
-
prerelease:
|
|
4
|
+
version: 1.1.2.rc1
|
|
5
|
+
prerelease: 6
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
8
8
|
- David North
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2012-
|
|
12
|
+
date: 2012-06-25 00:00:00.000000000Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: spree_core
|
|
16
|
-
requirement: &
|
|
16
|
+
requirement: &70249530014760 !ruby/object:Gem::Requirement
|
|
17
17
|
none: false
|
|
18
18
|
requirements:
|
|
19
19
|
- - =
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
|
-
version: 1.1.
|
|
21
|
+
version: 1.1.2.rc1
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
|
-
version_requirements: *
|
|
24
|
+
version_requirements: *70249530014760
|
|
25
25
|
- !ruby/object:Gem::Dependency
|
|
26
26
|
name: spree_auth
|
|
27
|
-
requirement: &
|
|
27
|
+
requirement: &70249530003060 !ruby/object:Gem::Requirement
|
|
28
28
|
none: false
|
|
29
29
|
requirements:
|
|
30
30
|
- - =
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 1.1.
|
|
32
|
+
version: 1.1.2.rc1
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
|
-
version_requirements: *
|
|
35
|
+
version_requirements: *70249530003060
|
|
36
36
|
description: Required dependency for Spree
|
|
37
37
|
email: david@spreecommerce.com
|
|
38
38
|
executables: []
|
|
@@ -127,12 +127,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
127
127
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
128
|
none: false
|
|
129
129
|
requirements:
|
|
130
|
-
- - ! '
|
|
130
|
+
- - ! '>'
|
|
131
131
|
- !ruby/object:Gem::Version
|
|
132
|
-
version:
|
|
133
|
-
segments:
|
|
134
|
-
- 0
|
|
135
|
-
hash: -2121439115021991443
|
|
132
|
+
version: 1.3.1
|
|
136
133
|
requirements:
|
|
137
134
|
- none
|
|
138
135
|
rubyforge_project:
|