My-Commerce_promo 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/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.rb +73 -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/spree_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 +101 -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,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,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,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
|
data/lib/spree_promo.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'spree_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 :spree_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['spree_promo:install:migrations'].invoke
|
5
|
+
Rake::Task['spree_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 :spree 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 :spree 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,101 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: My-Commerce_promo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Santa
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-23 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: My-Commerce_core
|
16
|
+
requirement: &82325120 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - =
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *82325120
|
25
|
+
description: Required dependancy for My-Commerce
|
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/tasks/promotions.rake
|
60
|
+
- lib/tasks/install.rake
|
61
|
+
- lib/tasks/promotions_extension_tasks.rake
|
62
|
+
- lib/spree_promo.rb
|
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
|
+
segments:
|
92
|
+
- 0
|
93
|
+
hash: -871181983
|
94
|
+
requirements:
|
95
|
+
- none
|
96
|
+
rubyforge_project: My-Commerce_promo
|
97
|
+
rubygems_version: 1.8.17
|
98
|
+
signing_key:
|
99
|
+
specification_version: 3
|
100
|
+
summary: Promotion functionality for use with My-Commerce.
|
101
|
+
test_files: []
|