apispree_promo 0.0.0
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/controllers/admin/promotion_rules_controller.rb +26 -0
- data/app/controllers/admin/promotions_controller.rb +21 -0
- data/app/controllers/orders_controller_decorator.rb +10 -0
- data/app/models/calculator/free_shipping.rb +24 -0
- data/app/models/promotion.rb +73 -0
- data/app/models/promotion/rules/first_order.rb +7 -0
- data/app/models/promotion/rules/item_total.rb +12 -0
- data/app/models/promotion/rules/product.rb +40 -0
- data/app/models/promotion/rules/user.rb +17 -0
- data/app/models/promotion_credit.rb +9 -0
- data/app/models/promotion_rule.rb +26 -0
- data/app/views/admin/promotion_rules/create.js.erb +6 -0
- data/app/views/admin/promotion_rules/destroy.js.erb +1 -0
- data/app/views/admin/promotions/_form.html.erb +46 -0
- data/app/views/admin/promotions/_promotion_rule.html.erb +8 -0
- data/app/views/admin/promotions/_tab.html.erb +1 -0
- data/app/views/admin/promotions/edit.html.erb +46 -0
- data/app/views/admin/promotions/index.html.erb +40 -0
- data/app/views/admin/promotions/new.html.erb +6 -0
- data/app/views/admin/promotions/rules/_first_order.html.erb +0 -0
- data/app/views/admin/promotions/rules/_item_total.html.erb +7 -0
- data/app/views/admin/promotions/rules/_product.html.erb +18 -0
- data/app/views/admin/promotions/rules/_user.html.erb +7 -0
- data/app/views/checkout/_coupon_code_field.html.erb +6 -0
- data/app/views/products/_promotions.html.erb +23 -0
- data/config/cucumber.yml +10 -0
- data/config/locales/en.yml +55 -0
- data/config/routes.rb +7 -0
- data/db/migrate/20100419190933_rename_coupons_to_promotions.rb +10 -0
- data/db/migrate/20100419194457_fix_existing_coupon_credits.rb +9 -0
- data/db/migrate/20100426100726_create_promotion_rules.rb +24 -0
- data/db/migrate/20100501084722_match_policy_for_promotions.rb +9 -0
- data/db/migrate/20100501095202_create_promotion_rules_users.rb +14 -0
- data/db/migrate/20100502163939_name_for_promotions.rb +9 -0
- data/db/migrate/20100923095305_update_calculable_type_for_promotions.rb +9 -0
- data/lib/apispree_promo.rb +132 -0
- data/lib/spree_promo_hooks.rb +11 -0
- data/lib/tasks/install.rake +26 -0
- data/lib/tasks/promotions.rake +11 -0
- data/lib/tasks/promotions_extension_tasks.rake +17 -0
- data/public/javascripts/admin/promotions.js +26 -0
- data/public/stylesheets/admin/promotions.css +54 -0
- metadata +98 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
class Admin::PromotionRulesController < Admin::BaseController
|
2
|
+
def create
|
3
|
+
@promotion = 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 edit_admin_promotion_path(@promotion)}
|
11
|
+
format.js { render :layout => false }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def destroy
|
16
|
+
@promotion = 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 edit_admin_promotion_path(@promotion)}
|
23
|
+
format.js { render :layout => false }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Admin::PromotionsController < Admin::ResourceController
|
2
|
+
before_filter :load_data
|
3
|
+
|
4
|
+
protected
|
5
|
+
|
6
|
+
def build_resource
|
7
|
+
@promotion = Promotion.new(params[:promotion])
|
8
|
+
if params[:promotion] && params[:promotion][:calculator_type]
|
9
|
+
@promotion.calculator = params[:promotion][:calculator_type].constantize.new
|
10
|
+
end
|
11
|
+
@promotion
|
12
|
+
end
|
13
|
+
|
14
|
+
def location_after_save
|
15
|
+
edit_admin_promotion_url(@promotion)
|
16
|
+
end
|
17
|
+
|
18
|
+
def load_data
|
19
|
+
@calculators = Promotion.calculators
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Calculator::FreeShipping < ::Calculator
|
2
|
+
|
3
|
+
def self.description
|
4
|
+
I18n.t("free_shipping")
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.register
|
8
|
+
super
|
9
|
+
Promotion.register_calculator(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
def compute(object)
|
13
|
+
if object.is_a?(Array)
|
14
|
+
return if object.empty?
|
15
|
+
order = object.first.order
|
16
|
+
else
|
17
|
+
order = object
|
18
|
+
end
|
19
|
+
|
20
|
+
order.ship_total
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,73 @@
|
|
1
|
+
class Promotion < ActiveRecord::Base
|
2
|
+
has_many :promotion_credits, :as => :source
|
3
|
+
calculated_adjustments
|
4
|
+
alias credits promotion_credits
|
5
|
+
|
6
|
+
has_many :promotion_rules, :autosave => true
|
7
|
+
accepts_nested_attributes_for :promotion_rules
|
8
|
+
alias_method :rules, :promotion_rules
|
9
|
+
|
10
|
+
validates :name, :presence => true
|
11
|
+
|
12
|
+
# 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
|
+
# is provided
|
14
|
+
def save(*)
|
15
|
+
if super
|
16
|
+
promotion_rules.each { |p| p.save }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
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
|
+
|
29
|
+
def expired?
|
30
|
+
starts_at && Time.now < starts_at ||
|
31
|
+
expires_at && Time.now > expires_at ||
|
32
|
+
usage_limit && credits_count >= usage_limit
|
33
|
+
end
|
34
|
+
|
35
|
+
def credits_count
|
36
|
+
credits.with_order.count
|
37
|
+
end
|
38
|
+
|
39
|
+
def rules_are_eligible?(order)
|
40
|
+
return true if rules.none?
|
41
|
+
if match_policy == 'all'
|
42
|
+
rules.all?{|r| r.eligible?(order)}
|
43
|
+
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
|
+
})
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def compute(order)
|
63
|
+
amount = calculator.compute(order)
|
64
|
+
amount = order.item_total if amount > order.item_total
|
65
|
+
-amount
|
66
|
+
end
|
67
|
+
|
68
|
+
# Products assigned to all product rules
|
69
|
+
def products
|
70
|
+
@products ||= rules.of_type("Promotion::Rules::Product").map(&:products).flatten.uniq
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# A rule to limit a promotion to a specific user.
|
2
|
+
class Promotion::Rules::ItemTotal < PromotionRule
|
3
|
+
|
4
|
+
preference :amount, :decimal, :default => 100.00
|
5
|
+
preference :operator, :string, :default => '>'
|
6
|
+
|
7
|
+
OPERATORS = ['gt', 'gte']
|
8
|
+
|
9
|
+
def eligible?(order)
|
10
|
+
order.item_total.send(preferred_operator == 'gte' ? :>= : :>, preferred_amount)
|
11
|
+
end
|
12
|
+
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
|
+
class Promotion::Rules::Product < PromotionRule
|
5
|
+
|
6
|
+
belongs_to :product_group
|
7
|
+
has_and_belongs_to_many :products, :class_name => '::Product', :join_table => '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)
|
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
|
+
|
27
|
+
def products_source=(source)
|
28
|
+
if source.to_s == 'manual'
|
29
|
+
self.product_group_id = nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def product_ids_string
|
34
|
+
product_ids.join(',')
|
35
|
+
end
|
36
|
+
def product_ids_string=(s)
|
37
|
+
self.product_ids = s.to_s.split(',').map(&:strip)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Promotion::Rules::User < PromotionRule
|
2
|
+
belongs_to :user
|
3
|
+
has_and_belongs_to_many :users, :class_name => '::User', :join_table => 'promotion_rules_users', :foreign_key => 'promotion_rule_id'
|
4
|
+
|
5
|
+
def eligible?(order)
|
6
|
+
users.none? or users.include?(order.user)
|
7
|
+
end
|
8
|
+
|
9
|
+
def user_ids_string
|
10
|
+
user_ids.join(',')
|
11
|
+
end
|
12
|
+
|
13
|
+
def user_ids_string=(s)
|
14
|
+
self.user_ids = s.to_s.split(',').map(&:strip)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class PromotionCredit < ::Adjustment
|
2
|
+
scope :with_order, :conditions => "order_id IS NOT NULL"
|
3
|
+
|
4
|
+
# Checks if credit is still applicable to order
|
5
|
+
# If source of adjustment is credit, it checks if it's still valid
|
6
|
+
def applicable?
|
7
|
+
source && source.eligible?(order) && super
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Base class for all promotion rules
|
2
|
+
class PromotionRule < ActiveRecord::Base
|
3
|
+
|
4
|
+
belongs_to :promotion
|
5
|
+
|
6
|
+
scope :of_type, lambda {|t| {:conditions => {:type => t}}}
|
7
|
+
|
8
|
+
def eligible?(order)
|
9
|
+
raise 'eligible? should be implemented in a sub-class of Promotion::PromotionRule'
|
10
|
+
end
|
11
|
+
|
12
|
+
@rule_classes = nil
|
13
|
+
@@rule_classes = Set.new
|
14
|
+
def self.register
|
15
|
+
@@rule_classes.add(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.rule_classes
|
19
|
+
@@rule_classes.to_a
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.rule_class_names
|
23
|
+
PromotionRule.rule_classes.map(&:name)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
jQuery('#rules').append('<%= escape_javascript( render(:partial => 'admin/promotions/promotion_rule', :object => @promotion_rule) ) %>');
|
2
|
+
jQuery('#<%= dom_id @promotion_rule %>').hide();
|
3
|
+
jQuery('#<%= dom_id @promotion_rule %>').fadeIn();
|
4
|
+
initProductRuleSourceField();
|
5
|
+
jQuery('#<%= dom_id @promotion_rule %> .tokeninput.products').productPicker();
|
6
|
+
jQuery('#<%= dom_id @promotion_rule %> .tokeninput.users').userPicker();
|
@@ -0,0 +1 @@
|
|
1
|
+
jQuery('#<%= dom_id @promotion_rule %>').fadeOut().remove();
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<%= render "shared/error_messages", :target => @promotion %>
|
2
|
+
<fieldset id="general_fields">
|
3
|
+
<legend><%= t(:general) %></legend>
|
4
|
+
<%= f.field_container :name do %>
|
5
|
+
<%= f.label :name %><br />
|
6
|
+
<%= f.text_field :name %>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<%= f.field_container :description do %>
|
10
|
+
<%= f.label :description %><br />
|
11
|
+
<%= f.text_area :description, :style => "height:50px" %>
|
12
|
+
<% end %>
|
13
|
+
|
14
|
+
<%= f.field_container :code do %>
|
15
|
+
<%= f.label :code %><br />
|
16
|
+
<%= f.text_field :code %>
|
17
|
+
<% end %>
|
18
|
+
|
19
|
+
<%= f.field_container :combine do %>
|
20
|
+
<label>
|
21
|
+
<%= f.check_box :combine %>
|
22
|
+
<%= t(:may_be_combined_with_other_promotions) %>
|
23
|
+
</label>
|
24
|
+
<% end %>
|
25
|
+
|
26
|
+
</fieldset>
|
27
|
+
|
28
|
+
<fieldset id="expiry_fields">
|
29
|
+
<legend><%= t(:expiry) %></legend>
|
30
|
+
<p>
|
31
|
+
<%= f.label :usage_limit %><br />
|
32
|
+
<%= f.text_field :usage_limit %>
|
33
|
+
</p>
|
34
|
+
|
35
|
+
<p id="starts_at_field">
|
36
|
+
<%= f.label :starts_at %>
|
37
|
+
<%= f.spree_date_picker :starts_at, :prompt => true, :use_month_numbers => true %>
|
38
|
+
</p>
|
39
|
+
<p id="expires_at_field">
|
40
|
+
<%= f.label :expires_at %>
|
41
|
+
<%= f.spree_date_picker :expires_at, :prompt => true, :use_month_numbers => true %>
|
42
|
+
</p>
|
43
|
+
</fieldset>
|
44
|
+
|
45
|
+
<hr />
|
46
|
+
<%= render "admin/shared/calculator_fields", :f => f %>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<div class="promotion-rule" id="<%= dom_id promotion_rule %>">
|
2
|
+
<span class="delete"><%= link_to_with_icon 'cross', '', admin_promotion_promotion_rule_path(@promotion, promotion_rule), 'data-remote' => true, 'data-method' => 'delete' %></span>
|
3
|
+
<% type_name = promotion_rule.class.name.demodulize.underscore %>
|
4
|
+
<% param_prefix = "promotion[promotion_rules_attributes][#{promotion_rule.id}]" %>
|
5
|
+
<%= hidden_field_tag "#{param_prefix}[id]", promotion_rule.id %>
|
6
|
+
<p><%= t "promotion_rule_types.#{type_name}.description" %></p>
|
7
|
+
<%= render "admin/promotions/rules/#{type_name}", :promotion_rule => promotion_rule, :param_prefix => param_prefix %>
|
8
|
+
</div>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= tab(:promotions) %>
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<h1><%= t("editing_promotion") %></h1>
|
2
|
+
|
3
|
+
<%= form_for(@promotion, :url => object_url, :html => { :method => :put }) do |f| %>
|
4
|
+
<%= render :partial => "form", :locals => { :f => f } %>
|
5
|
+
<%= render "admin/shared/edit_resource_links" %>
|
6
|
+
<% end %>
|
7
|
+
|
8
|
+
<fieldset id="rule_fields">
|
9
|
+
<legend><%= t(:rules) %></legend>
|
10
|
+
|
11
|
+
<%= form_for(@promotion, :url => object_url, :html => { :method => :put }) do |f| %>
|
12
|
+
<p>
|
13
|
+
<% for policy in Promotion::MATCH_POLICIES %>
|
14
|
+
<label><%= f.radio_button :match_policy, policy %> <%= t "promotion_form.match_policies.#{policy}" %></label>
|
15
|
+
<% end %>
|
16
|
+
</p>
|
17
|
+
<div id="rules" class="filter_list">
|
18
|
+
<% if @promotion.rules.any? %>
|
19
|
+
<%= render :partial => 'promotion_rule', :collection => @promotion.rules %>
|
20
|
+
<% else %>
|
21
|
+
<!-- <p><%= t('no_rules_added') %></p> -->
|
22
|
+
<% end %>
|
23
|
+
</div>
|
24
|
+
<p class="form-buttons">
|
25
|
+
<%= button t("update") %>
|
26
|
+
</p>
|
27
|
+
<% end %>
|
28
|
+
|
29
|
+
<%= form_tag(admin_promotion_promotion_rules_path(@promotion), 'data-remote' => true,
|
30
|
+
:id => 'new_product_rule_form') do %>
|
31
|
+
<% options = options_for_select( PromotionRule.rule_class_names.map {|name| [ t("promotion_rule_types.#{name.demodulize.underscore}.name"), name] } ) %>
|
32
|
+
<p>
|
33
|
+
<label for="promotion_rule_type"><%= t('add_rule_of_type') %></label>
|
34
|
+
<%= select_tag("promotion_rule[type]", options) %>
|
35
|
+
<input type="submit" value="<%= t('add') %>" />
|
36
|
+
</p>
|
37
|
+
<% end %>
|
38
|
+
|
39
|
+
</fieldset>
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
<% content_for :head do %>
|
44
|
+
<%= javascript_include_tag 'admin/promotions.js' %>
|
45
|
+
<%= stylesheet_link_tag 'admin/promotions.css' %>
|
46
|
+
<% end %>
|
@@ -0,0 +1,40 @@
|
|
1
|
+
<div class='toolbar'>
|
2
|
+
<ul class='actions'>
|
3
|
+
<li>
|
4
|
+
<%= button_link_to t("new_promotion"), new_object_url, :icon => 'add' %>
|
5
|
+
</li>
|
6
|
+
</ul>
|
7
|
+
<br class='clear' />
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<h1><%= t("promotions") %></h1>
|
11
|
+
|
12
|
+
<table class="index">
|
13
|
+
<thead>
|
14
|
+
<tr>
|
15
|
+
<th><%= t("code") %></th>
|
16
|
+
<th><%= t("description") %></th>
|
17
|
+
<th><%= t("combine") %></th>
|
18
|
+
<th><%= t("usage_limit") %></th>
|
19
|
+
<th><%= t("expiration") %></th>
|
20
|
+
<th><%= t("calculator") %></th>
|
21
|
+
<th width="150"><%= t("action") %></th>
|
22
|
+
</tr>
|
23
|
+
</thead>
|
24
|
+
<tbody>
|
25
|
+
<% for promotion in @promotions %>
|
26
|
+
<tr id="<%= dom_id promotion %>">
|
27
|
+
<td><%= promotion.code %></td>
|
28
|
+
<td><%= promotion.description %></td>
|
29
|
+
<td><%= promotion.combine ? t(:yes) : t(:no) %></td>
|
30
|
+
<td><%= promotion.usage_limit %></td>
|
31
|
+
<td><%= promotion.expires_at.to_date.to_s(:short_date) if promotion.expires_at %></td>
|
32
|
+
<td><%= promotion.calculator.description if promotion.calculator %></td>
|
33
|
+
<td>
|
34
|
+
<%= link_to_edit promotion %>
|
35
|
+
<%= link_to_delete promotion %>
|
36
|
+
</td>
|
37
|
+
</tr>
|
38
|
+
<% end %>
|
39
|
+
</tbody>
|
40
|
+
</table>
|
File without changes
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<p class="field">
|
2
|
+
<label for="<%= "#{param_prefix}_preferred_amount" %>">
|
3
|
+
Item total must be
|
4
|
+
<%= select_tag "#{param_prefix}[preferred_operator]", options_for_select(Promotion::Rules::ItemTotal::OPERATORS.map{|o| [t("item_total_rule.operators.#{o}"),o]}, promotion_rule.preferred_operator) %>
|
5
|
+
<%= text_field_tag "#{param_prefix}[preferred_amount]", promotion_rule.preferred_amount, :size => 10 %>
|
6
|
+
</label>
|
7
|
+
</p>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<p class="field products_rule_products_source_field">
|
2
|
+
<label><%= radio_button_tag "#{param_prefix}[products_source]", :manual, promotion_rule.product_group.nil? %> <%= t "product_rule.product_source.manual" %></label>
|
3
|
+
<label><%= radio_button_tag "#{param_prefix}[products_source]", :group, promotion_rule.product_group.present? %> <%= t "product_rule.product_source.group" %></label>
|
4
|
+
</p>
|
5
|
+
<p class="field products_rule_product_group">
|
6
|
+
<label><%= t('product_group') %><br />
|
7
|
+
<%= select_tag "#{param_prefix}[product_group_id]", options_from_collection_for_select(ProductGroup.all, :id, :name, promotion_rule.product_group_id) %></label>
|
8
|
+
</p>
|
9
|
+
<p class="field products_rule_products">
|
10
|
+
<label>
|
11
|
+
<%= t('product_rule.choose_products') %><br />
|
12
|
+
<%= product_picker_field "#{param_prefix}[product_ids_string]", promotion_rule.product_ids_string %>
|
13
|
+
</p>
|
14
|
+
<p>
|
15
|
+
<label>
|
16
|
+
<%= t("product_rule.label", :select => select_tag("#{param_prefix}[preferred_match_policy]", options_for_select(Promotion::Rules::Product::MATCH_POLICIES.map{|s| [t("product_rule.match_#{s}"),s] }, promotion_rule.preferred_match_policy))).html_safe %>
|
17
|
+
</label>
|
18
|
+
</p>
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<p class="field">
|
2
|
+
<label>
|
3
|
+
<%= t('user_rule.choose_users') %><br />
|
4
|
+
<% user_names_hash = promotion_rule.users.inject({}){|memo,item| memo[item.id] = item.email; memo} %>
|
5
|
+
<input type="text" name="<%= param_prefix %>[user_ids_string]" value="<%= promotion_rule.user_ids_string %>" class="tokeninput users" data-names='<%= user_names_hash.to_json %>' />
|
6
|
+
</label>
|
7
|
+
</p>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<% promotions = @product.possible_promotions %>
|
2
|
+
<% if promotions.any? %>
|
3
|
+
<div id="promotions">
|
4
|
+
<h3><%= t 'promotions' %></h3>
|
5
|
+
|
6
|
+
<% for promotion in promotions %>
|
7
|
+
<div>
|
8
|
+
<h4><%= promotion.name %></h4>
|
9
|
+
<p><%= promotion.description %></p>
|
10
|
+
<% if promotion.products.any? %>
|
11
|
+
<ul>
|
12
|
+
<% for product in promotion.products %>
|
13
|
+
<li><%= link_to product.name, product_path(product) %></li>
|
14
|
+
<% end %>
|
15
|
+
</ul>
|
16
|
+
<% end %>
|
17
|
+
</div>
|
18
|
+
<% end %>
|
19
|
+
|
20
|
+
</div>
|
21
|
+
<% end %>
|
22
|
+
|
23
|
+
|
data/config/cucumber.yml
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
<%
|
2
|
+
rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : ""
|
3
|
+
rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}"
|
4
|
+
std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} --strict --tags ~@wip"
|
5
|
+
ci_opts = "--format progress --strict"
|
6
|
+
%>
|
7
|
+
default: <%= std_opts %> features
|
8
|
+
wip: --tags @wip:3 --wip features
|
9
|
+
ci: <%= ci_opts %> features CI=true
|
10
|
+
rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip
|
@@ -0,0 +1,55 @@
|
|
1
|
+
---
|
2
|
+
en:
|
3
|
+
activerecord:
|
4
|
+
attributes:
|
5
|
+
promotion:
|
6
|
+
name: "Name"
|
7
|
+
description: "Description"
|
8
|
+
code: "Code"
|
9
|
+
usage_limit: "Usage limit"
|
10
|
+
starts_at: "Starts at"
|
11
|
+
expires_at: "Expires at"
|
12
|
+
add_rule_of_type: Add rule of type
|
13
|
+
coupon: Coupon
|
14
|
+
coupon_code: Coupon code
|
15
|
+
editing_promotion: Editing Promotion
|
16
|
+
expiry: Expiry
|
17
|
+
free_shipping: Free Shipping
|
18
|
+
may_be_combined_with_other_promotions: May be combined with other promotions
|
19
|
+
new_promotion: New Promotion
|
20
|
+
no_rules_added: No rules added
|
21
|
+
promotion: Promotion
|
22
|
+
promotions: Promotions
|
23
|
+
promotion_form:
|
24
|
+
match_policies:
|
25
|
+
all: Match any of these rules
|
26
|
+
any: Match all of these rules
|
27
|
+
promotions_description: Manage offers and coupons with promotions
|
28
|
+
promotion_rule_types:
|
29
|
+
user:
|
30
|
+
name: User
|
31
|
+
description: Available only to the specified users
|
32
|
+
product:
|
33
|
+
name: Product(s)
|
34
|
+
description: Order includes specified product(s)
|
35
|
+
item_total:
|
36
|
+
name: Item total
|
37
|
+
description: Order total meets these criteria
|
38
|
+
first_order:
|
39
|
+
name: First order
|
40
|
+
description: Must be the customer's first order
|
41
|
+
product_rule:
|
42
|
+
choose_products: Choose products
|
43
|
+
label: "Order must contain %{select} of these products"
|
44
|
+
match_any: at least one
|
45
|
+
match_all: all
|
46
|
+
product_source:
|
47
|
+
group: From product group
|
48
|
+
manual: Manually choose
|
49
|
+
rules: Rules
|
50
|
+
user_rule:
|
51
|
+
choose_users: Choose users
|
52
|
+
item_total_rule:
|
53
|
+
operators:
|
54
|
+
gt: greater than
|
55
|
+
gte: greater than or equal to
|
data/config/routes.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
class FixExistingCouponCredits < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
execute("UPDATE adjustments SET type='PromotionCredit' WHERE type='CouponCredit'")
|
4
|
+
execute("UPDATE adjustments SET adjustment_source_type='Promotion' WHERE adjustment_source_type='Coupon'")
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.down
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class CreatePromotionRules < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :promotion_rules do |t|
|
4
|
+
t.references :promotion, :user, :product_group
|
5
|
+
t.timestamps
|
6
|
+
t.string :type
|
7
|
+
end
|
8
|
+
add_index :promotion_rules, :product_group_id
|
9
|
+
add_index :promotion_rules, :user_id
|
10
|
+
|
11
|
+
create_table :products_promotion_rules do |t|
|
12
|
+
t.integer :product_id, :promotion_rule_id
|
13
|
+
end
|
14
|
+
remove_column :products_promotion_rules, :id
|
15
|
+
add_index :products_promotion_rules, :product_id
|
16
|
+
add_index :products_promotion_rules, :promotion_rule_id
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.down
|
21
|
+
drop_table :promotion_rules
|
22
|
+
drop_table :products_promotion_rules
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreatePromotionRulesUsers < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :promotion_rules_users do |t|
|
4
|
+
t.integer :user_id, :promotion_rule_id
|
5
|
+
end
|
6
|
+
remove_column :promotion_rules_users, :id
|
7
|
+
add_index :promotion_rules_users, :user_id
|
8
|
+
add_index :promotion_rules_users, :promotion_rule_id
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.down
|
12
|
+
drop_table :promotion_rules_users
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class UpdateCalculableTypeForPromotions < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
Calculator.update_all("calculable_type = 'Promotion'", "calculable_type = 'Coupon'")
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.down
|
7
|
+
Calculator.update_all("calculable_type = 'Coupon'", "calculable_type = 'Promotion'")
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'apispree_core'
|
2
|
+
require 'spree_promo_hooks'
|
3
|
+
|
4
|
+
module SpreePromo
|
5
|
+
class Engine < Rails::Engine
|
6
|
+
def self.activate
|
7
|
+
# put class_eval and other logic that depends on classes outside of the engine inside this block
|
8
|
+
Product.class_eval do
|
9
|
+
has_and_belongs_to_many :promotion_rules
|
10
|
+
|
11
|
+
def possible_promotions
|
12
|
+
rules_with_matching_product_groups = product_groups.map(&:promotion_rules).flatten
|
13
|
+
all_rules = promotion_rules + rules_with_matching_product_groups
|
14
|
+
promotion_ids = all_rules.map(&:promotion_id).uniq
|
15
|
+
Promotion.automatic.scoped(:conditions => {:id => promotion_ids})
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ProductGroup.class_eval do
|
20
|
+
has_many :promotion_rules
|
21
|
+
end
|
22
|
+
|
23
|
+
Order.class_eval do
|
24
|
+
|
25
|
+
has_many :promotion_credits, :conditions => "source_type='Promotion'", :dependent => :destroy
|
26
|
+
|
27
|
+
attr_accessible :coupon_code
|
28
|
+
attr_accessor :coupon_code
|
29
|
+
before_save :process_coupon_code, :if => "@coupon_code.present?"
|
30
|
+
|
31
|
+
def finalized?
|
32
|
+
self.class.finalized_states.include?(state)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.finalized_states
|
36
|
+
["complete", "awaiting_return", "returned"]
|
37
|
+
end
|
38
|
+
|
39
|
+
def promotion_credit_exists?(credit)
|
40
|
+
promotion_credits.reload.detect { |c| c.source_id == credit.id }
|
41
|
+
end
|
42
|
+
|
43
|
+
def process_coupon_code
|
44
|
+
coupon = Promotion.find(:first, :conditions => ["UPPER(code) = ?", @coupon_code.upcase])
|
45
|
+
if coupon
|
46
|
+
coupon.create_discount(self)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def products
|
51
|
+
line_items.map {|li| li.variant.product}
|
52
|
+
end
|
53
|
+
|
54
|
+
def update_totals(force_adjustment_recalculation=false)
|
55
|
+
self.payment_total = payments.completed.map(&:amount).sum
|
56
|
+
self.item_total = line_items.map(&:amount).sum
|
57
|
+
|
58
|
+
process_automatic_promotions unless finalized?
|
59
|
+
|
60
|
+
if force_adjustment_recalculation
|
61
|
+
applicable_adjustments, adjustments_to_destroy = adjustments.partition{|a| a.applicable?}
|
62
|
+
self.adjustments = applicable_adjustments
|
63
|
+
adjustments_to_destroy.each(&:destroy)
|
64
|
+
end
|
65
|
+
|
66
|
+
self.adjustment_total = self.adjustments.map(&:amount).sum
|
67
|
+
self.total = self.item_total + self.adjustment_total
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def process_automatic_promotions
|
72
|
+
# recalculate amount
|
73
|
+
self.promotion_credits.each do |credit|
|
74
|
+
if credit.source.eligible?(self)
|
75
|
+
amount = credit.source.compute(self)
|
76
|
+
if credit.amount != amount
|
77
|
+
# avoid infinite callbacks
|
78
|
+
PromotionCredit.update_all("amount = #{amount}", { :id => credit.id })
|
79
|
+
end
|
80
|
+
else
|
81
|
+
credit.destroy
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
current_promotions = self.promotion_credits.map(&:source)
|
86
|
+
# return if current promotions can not be combined
|
87
|
+
return if current_promotions.any? { |promotion| !promotion.combine? }
|
88
|
+
|
89
|
+
new_promotions = eligible_automatic_promotions - current_promotions
|
90
|
+
new_promotions.each do |promotion|
|
91
|
+
next if current_promotions.present? && !promotion.combine?
|
92
|
+
amount = credit.source.compute(self)
|
93
|
+
if amount > 0
|
94
|
+
self.promotion_credits.create(
|
95
|
+
:source => promotion,
|
96
|
+
:amount => amount,
|
97
|
+
:label => promotion.name
|
98
|
+
)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def eligible_automatic_promotions
|
104
|
+
@eligible_automatic_coupons ||= Promotion.automatic.select{|c| c.eligible?(self)}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
if File.basename( $0 ) != "rake"
|
109
|
+
# register promotion rules
|
110
|
+
[Promotion::Rules::ItemTotal, Promotion::Rules::Product, Promotion::Rules::User, Promotion::Rules::FirstOrder].each &:register
|
111
|
+
|
112
|
+
# register default promotion calculators
|
113
|
+
[
|
114
|
+
Calculator::FlatPercentItemTotal,
|
115
|
+
Calculator::FlatRate,
|
116
|
+
Calculator::FlexiRate,
|
117
|
+
Calculator::PerItem,
|
118
|
+
Calculator::FreeShipping
|
119
|
+
].each{|c_model|
|
120
|
+
begin
|
121
|
+
Promotion.register_calculator(c_model) if c_model.table_exists?
|
122
|
+
rescue Exception => e
|
123
|
+
$stderr.puts "Error registering promotion calculator #{c_model}"
|
124
|
+
end
|
125
|
+
}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
config.autoload_paths += %W(#{config.root}/lib)
|
130
|
+
config.to_prepare &method(:activate).to_proc
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
namespace :apispree_promo do
|
2
|
+
desc "Copies all migrations and assets (NOTE: This will be obsolete with Rails 3.1)"
|
3
|
+
task :install do
|
4
|
+
Rake::Task['apispree_promo:install:migrations'].invoke
|
5
|
+
Rake::Task['apispree_promo:install:assets'].invoke
|
6
|
+
end
|
7
|
+
|
8
|
+
namespace :install do
|
9
|
+
|
10
|
+
desc "Copies all migrations (NOTE: This will be obsolete with Rails 3.1)"
|
11
|
+
task :migrations do
|
12
|
+
source = File.join(File.dirname(__FILE__), '..', '..', 'db')
|
13
|
+
destination = File.join(Rails.root, 'db')
|
14
|
+
Spree::FileUtilz.mirror_files(source, destination)
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Copies all assets (NOTE: This will be obsolete with Rails 3.1)"
|
18
|
+
task :assets do
|
19
|
+
source = File.join(File.dirname(__FILE__), '..', '..', 'public')
|
20
|
+
destination = File.join(Rails.root, 'public')
|
21
|
+
puts "INFO: Mirroring assets from #{source} to #{destination}"
|
22
|
+
Spree::FileUtilz.mirror_files(source, destination)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
namespace :apispree do
|
2
|
+
desc "Synchronize public assets, migrations, seed and sample data from the Spree gems"
|
3
|
+
task :sync do
|
4
|
+
public_dir = File.join(File.dirname(__FILE__), '..', '..', 'public')
|
5
|
+
migration_dir = File.join(File.dirname(__FILE__), '..', '..', 'db', 'migrate')
|
6
|
+
puts "Mirror: #{public_dir}"
|
7
|
+
Spree::FileUtilz.mirror_with_backup(public_dir, File.join(Rails.root, 'public'))
|
8
|
+
puts "Mirror: #{migration_dir}"
|
9
|
+
Spree::FileUtilz.mirror_with_backup(migration_dir, File.join(Rails.root, 'db', 'migrate'))
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
namespace :apispree do
|
2
|
+
namespace :extensions do
|
3
|
+
namespace :promotions do
|
4
|
+
desc "Copies public assets of the Promotions to the instance public/ directory."
|
5
|
+
task :update => :environment do
|
6
|
+
is_svn_git_or_dir = proc {|path| path =~ /\.svn/ || path =~ /\.git/ || File.directory?(path) }
|
7
|
+
Dir[PromotionsExtension.root + "/public/**/*"].reject(&is_svn_git_or_dir).each do |file|
|
8
|
+
path = file.sub(PromotionsExtension.root, '')
|
9
|
+
directory = File.dirname(path)
|
10
|
+
puts "Copying #{path}..."
|
11
|
+
mkdir_p Rails.root + directory
|
12
|
+
cp file, Rails.root + path
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
var initProductRuleSourceField = function(){
|
2
|
+
|
3
|
+
$products_source_field = jQuery('.products_rule_products_source_field input');
|
4
|
+
$products_source_field.click(function() {
|
5
|
+
$rule_container = jQuery(this).parents('.promotion-rule');
|
6
|
+
if(this.checked){
|
7
|
+
if(this.value == 'manual'){
|
8
|
+
$rule_container.find('.products_rule_products').show();
|
9
|
+
$rule_container.find('.products_rule_product_group').hide();
|
10
|
+
} else {
|
11
|
+
$rule_container.find('.products_rule_products').hide();
|
12
|
+
$rule_container.find('.products_rule_product_group').show();
|
13
|
+
}
|
14
|
+
}
|
15
|
+
});
|
16
|
+
$products_source_field.each(function() {
|
17
|
+
$(this).triggerHandler('click');
|
18
|
+
});
|
19
|
+
|
20
|
+
};
|
21
|
+
|
22
|
+
|
23
|
+
jQuery(document).ready(function() {
|
24
|
+
initProductRuleSourceField();
|
25
|
+
});
|
26
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
.promotion-rule {
|
2
|
+
-moz-border-radius: 5px;
|
3
|
+
-webkit-border-radius: 5px;
|
4
|
+
padding: 8px 40px 8px 10px;
|
5
|
+
background-color: #eee;
|
6
|
+
border-bottom: 1px solid #bbb;
|
7
|
+
margin-bottom: 4px;
|
8
|
+
position: relative;
|
9
|
+
}
|
10
|
+
.promotion-rule .delete {
|
11
|
+
display: block;
|
12
|
+
position: absolute;
|
13
|
+
right: 10px;
|
14
|
+
top: 10px;
|
15
|
+
}
|
16
|
+
|
17
|
+
|
18
|
+
.products_rule_products_source_field label {
|
19
|
+
display: inline;
|
20
|
+
}
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
.edit_promotion #general_fields, .edit_promotion #expiry_fields {
|
25
|
+
float: left;
|
26
|
+
width: 46%;
|
27
|
+
}
|
28
|
+
|
29
|
+
.edit_promotion #general_fields {
|
30
|
+
margin-right: 10px;
|
31
|
+
}
|
32
|
+
|
33
|
+
.edit_promotion #expiry_fields {
|
34
|
+
|
35
|
+
}
|
36
|
+
|
37
|
+
.edit_promotion #starts_at_field, .edit_promotion #expires_at_field {
|
38
|
+
width: 200px;
|
39
|
+
float: left;
|
40
|
+
clear: none;
|
41
|
+
}
|
42
|
+
.edit_promotion #starts_at_field label, .edit_promotion #expires_at_field label{
|
43
|
+
display: block;
|
44
|
+
}
|
45
|
+
#rule_fields {
|
46
|
+
position: relative;
|
47
|
+
}
|
48
|
+
#new_product_rule_form {
|
49
|
+
position: absolute;
|
50
|
+
text-align:right;
|
51
|
+
width: 300px;
|
52
|
+
right: 30px;
|
53
|
+
bottom: 20px;
|
54
|
+
}
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apispree_promo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Santosh Kumar Mohanty
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-26 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: apispree_core
|
16
|
+
requirement: &76370210 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - =
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *76370210
|
25
|
+
description: Required dependancy for API-Spree
|
26
|
+
email: santa.jyp@gmail.com
|
27
|
+
executables: []
|
28
|
+
extensions: []
|
29
|
+
extra_rdoc_files: []
|
30
|
+
files:
|
31
|
+
- app/models/promotion/rules/product.rb
|
32
|
+
- app/models/promotion/rules/item_total.rb
|
33
|
+
- app/models/promotion/rules/user.rb
|
34
|
+
- app/models/promotion/rules/first_order.rb
|
35
|
+
- app/models/promotion_rule.rb
|
36
|
+
- app/models/promotion.rb
|
37
|
+
- app/models/promotion_credit.rb
|
38
|
+
- app/models/calculator/free_shipping.rb
|
39
|
+
- app/controllers/admin/promotions_controller.rb
|
40
|
+
- app/controllers/admin/promotion_rules_controller.rb
|
41
|
+
- app/controllers/orders_controller_decorator.rb
|
42
|
+
- app/views/admin/promotions/index.html.erb
|
43
|
+
- app/views/admin/promotions/_promotion_rule.html.erb
|
44
|
+
- app/views/admin/promotions/rules/_first_order.html.erb
|
45
|
+
- app/views/admin/promotions/rules/_item_total.html.erb
|
46
|
+
- app/views/admin/promotions/rules/_user.html.erb
|
47
|
+
- app/views/admin/promotions/rules/_product.html.erb
|
48
|
+
- app/views/admin/promotions/_tab.html.erb
|
49
|
+
- app/views/admin/promotions/edit.html.erb
|
50
|
+
- app/views/admin/promotions/new.html.erb
|
51
|
+
- app/views/admin/promotions/_form.html.erb
|
52
|
+
- app/views/admin/promotion_rules/create.js.erb
|
53
|
+
- app/views/admin/promotion_rules/destroy.js.erb
|
54
|
+
- app/views/checkout/_coupon_code_field.html.erb
|
55
|
+
- app/views/products/_promotions.html.erb
|
56
|
+
- config/cucumber.yml
|
57
|
+
- config/routes.rb
|
58
|
+
- config/locales/en.yml
|
59
|
+
- lib/apispree_promo.rb
|
60
|
+
- lib/tasks/promotions.rake
|
61
|
+
- lib/tasks/install.rake
|
62
|
+
- lib/tasks/promotions_extension_tasks.rake
|
63
|
+
- lib/spree_promo_hooks.rb
|
64
|
+
- db/migrate/20100501095202_create_promotion_rules_users.rb
|
65
|
+
- db/migrate/20100502163939_name_for_promotions.rb
|
66
|
+
- db/migrate/20100501084722_match_policy_for_promotions.rb
|
67
|
+
- db/migrate/20100923095305_update_calculable_type_for_promotions.rb
|
68
|
+
- db/migrate/20100426100726_create_promotion_rules.rb
|
69
|
+
- db/migrate/20100419190933_rename_coupons_to_promotions.rb
|
70
|
+
- db/migrate/20100419194457_fix_existing_coupon_credits.rb
|
71
|
+
- public/javascripts/admin/promotions.js
|
72
|
+
- public/stylesheets/admin/promotions.css
|
73
|
+
homepage: http://spreecommerce.com
|
74
|
+
licenses: []
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ! '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: 1.8.7
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements:
|
92
|
+
- none
|
93
|
+
rubyforge_project: apispree_promo
|
94
|
+
rubygems_version: 1.8.17
|
95
|
+
signing_key:
|
96
|
+
specification_version: 3
|
97
|
+
summary: Promotion functionality for use with API-Spree.
|
98
|
+
test_files: []
|