spree_promotion_codes 3.0.6 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +24 -0
- data/Gemfile +6 -0
- data/LICENSE +26 -0
- data/README.md +42 -0
- data/Rakefile +21 -0
- data/app/controllers/spree/admin/promotion_codes_controller.rb +7 -0
- data/app/helpers/spree/admin/promotion_codes_helper.rb +13 -0
- data/app/models/spree.rb +5 -0
- data/app/models/spree/order_decorator.rb +36 -0
- data/app/models/spree/order_promotion.rb +4 -0
- data/app/models/spree/promotion_code.rb +43 -0
- data/app/models/spree/promotion_decorator.rb +71 -0
- data/app/models/spree/promotion_handler/cart_decorator.rb +24 -0
- data/app/models/spree/promotion_handler/coupon_decorator.rb +52 -0
- data/app/models/spree/promotion_handler/free_shipping_decorator.rb +9 -0
- data/app/models/spree/user_decorator.rb +3 -0
- data/app/overrides/orders/edit_override.rb +4 -0
- data/app/overrides/orders/index_override.rb +4 -0
- data/app/overrides/promotions/index_override.rb +16 -0
- data/app/overrides/promotions/manage_codes.rb +10 -0
- data/app/views/spree/admin/promotion_codes/_form.html.erb +29 -0
- data/app/views/spree/admin/promotion_codes/edit.html.erb +12 -0
- data/app/views/spree/admin/promotion_codes/index.html.erb +41 -0
- data/app/views/spree/admin/promotion_codes/new.html.erb +12 -0
- data/app/views/spree/admin/shared/_edit_orders.html.erb +17 -0
- data/app/views/spree/admin/shared/_index_orders_search.html.erb +8 -0
- data/app/views/spree/admin/shared/_index_promotions_table.html.erb +27 -0
- data/bin/rails +7 -0
- data/config/locales/en.yml +15 -0
- data/config/locales/es.yml +15 -0
- data/config/routes.rb +7 -0
- data/db/migrate/20161027141141_create_spree_promotion_codes.rb +10 -0
- data/db/migrate/20161028123452_add_disabled_to_promotion_codes.rb +5 -0
- data/db/migrate/20161101192534_move_code_to_promotion_code.rb +9 -0
- data/db/migrate/20161101192804_remove_code_from_promotion.rb +5 -0
- data/db/migrate/20161103111824_add_missing_indexes_to_promotion_codes.rb +6 -0
- data/db/migrate/20161103112910_add_promotion_code_to_orders_promotion.rb +6 -0
- data/db/migrate/20161104092718_add_usage_limit_to_promotion_code.rb +5 -0
- data/lib/generators/spree_promotion_codes/install/install_generator.rb +21 -0
- data/lib/spree_promotion_codes.rb +2 -0
- data/lib/spree_promotion_codes/engine.rb +20 -0
- data/lib/spree_promotion_codes/factories.rb +6 -0
- data/spec/models/spree/promotion_decorator_spec.rb +35 -0
- data/spec/models/spree/promotion_handler/coupon_decorator_spec.rb +325 -0
- data/spec/rails_helper.rb +57 -0
- data/spec/spec_helper.rb +94 -0
- data/spree_promotion_codes.gemspec +33 -0
- metadata +57 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04239c8ac7e88097d26826b991038becf9f4da69
|
4
|
+
data.tar.gz: 13c944c77025b4c659ddea190e020a1f06ce94ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb328a7dd93a8e395c34a95fe826c4920e82b134032a2ff6538b10369edea3b2722c67293d032ca16d925b285d6bff1ce116a6208cc5fdcb3ad0574e259cdf4c
|
7
|
+
data.tar.gz: 2954ba07f253937f8763ae5a5ad0b07fab490bacd8da28c05e6e5eb87deec44654de32a161d59545a0d4c696a6bf38637c5532476b9e7ac3001c72729c0e80ac
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
language: ruby
|
2
|
+
|
3
|
+
rvm:
|
4
|
+
- 2.2.3
|
5
|
+
- 2.3.0
|
6
|
+
|
7
|
+
sudo: false
|
8
|
+
|
9
|
+
cache: bundler
|
10
|
+
|
11
|
+
before_script:
|
12
|
+
- sh -e /etc/init.d/xvfb start
|
13
|
+
- bundle exec rake test_app
|
14
|
+
|
15
|
+
after_script:
|
16
|
+
- bundle exec codeclimate-test-reporter
|
17
|
+
|
18
|
+
script:
|
19
|
+
- export DISPLAY=:99.0
|
20
|
+
- bundle exec rspec spec
|
21
|
+
|
22
|
+
addons:
|
23
|
+
code_climate:
|
24
|
+
repo_token: d57b42229ebd9950493107b4ea3cedc95e4f3e9a3ce011f9e8c006486be960de
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Copyright (c) 2016 [name of plugin creator]
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without modification,
|
5
|
+
are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
* Redistributions of source code must retain the above copyright notice,
|
8
|
+
this list of conditions and the following disclaimer.
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
* Neither the name Spree nor the names of its contributors may be used to
|
13
|
+
endorse or promote products derived from this software without specific
|
14
|
+
prior written permission.
|
15
|
+
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
17
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
18
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
19
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
20
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
21
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
22
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
23
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
24
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
25
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
26
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
SpreePromotionCodes
|
2
|
+
===================
|
3
|
+
<a href="https://codeclimate.com/github/epergo/spree_promotion_codes"><img src="https://codeclimate.com/github/epergo/spree_promotion_codes/badges/gpa.svg"
|
4
|
+
/></a>
|
5
|
+
<a
|
6
|
+
href="https://codeclimate.com/github/epergo/spree_promotion_codes/coverage"><img
|
7
|
+
src="https://codeclimate.com/github/epergo/spree_promotion_codes/badges/coverage.svg"
|
8
|
+
/></a>
|
9
|
+
<a href="https://travis-ci.org/epergo/spree_promotion_codes"><img
|
10
|
+
src="https://travis-ci.org/epergo/spree_promotion_codes.svg?branch=master"/></a>
|
11
|
+
|
12
|
+
Spree extension to allow promotions to be reedemed with more than 1 code
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
1. Add this extension to your Gemfile with this line:
|
17
|
+
```ruby
|
18
|
+
gem 'spree_promotion_codes', github: 'epergo/spree_promotion_codes', branch: '3-0-stable'
|
19
|
+
```
|
20
|
+
2. Install the gem using Bundler:
|
21
|
+
```ruby
|
22
|
+
bundle install
|
23
|
+
```
|
24
|
+
|
25
|
+
3. Copy & run migrations
|
26
|
+
```ruby
|
27
|
+
bundle exec rails g spree_promotion_codes:install
|
28
|
+
```
|
29
|
+
|
30
|
+
4. Restart your server
|
31
|
+
|
32
|
+
If your server was running, restart it so that it can find the assets properly.
|
33
|
+
|
34
|
+
## Testing
|
35
|
+
|
36
|
+
First bundle your dependencies, then run `rake`. `rake` will default to building the dummy app if it does not exist, then it will run specs. The dummy app can be regenerated by using `rake test_app`.
|
37
|
+
|
38
|
+
```shell
|
39
|
+
bundle
|
40
|
+
bundle exec rake
|
41
|
+
```
|
42
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'spree/testing_support/extension_rake'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new
|
8
|
+
|
9
|
+
task :default do
|
10
|
+
if Dir["spec/dummy"].empty?
|
11
|
+
Rake::Task[:test_app].invoke
|
12
|
+
Dir.chdir("../../")
|
13
|
+
end
|
14
|
+
Rake::Task[:spec].invoke
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Generates a dummy app for testing'
|
18
|
+
task :test_app do
|
19
|
+
ENV['LIB_NAME'] = 'spree_promotion_codes'
|
20
|
+
Rake::Task['extension:test_app'].invoke
|
21
|
+
end
|
data/app/models/spree.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
Spree::Order.class_eval do
|
2
|
+
has_many :order_promotions
|
3
|
+
has_many :promotions, through: :order_promotions
|
4
|
+
|
5
|
+
def self.ransackable_scopes(auth_object = nil)
|
6
|
+
%w(with_promotion_code)
|
7
|
+
end
|
8
|
+
|
9
|
+
scope :with_promotion_code, -> (promotion_code) do
|
10
|
+
promotion_code = Spree::PromotionCode.find_by(code: promotion_code.downcase)
|
11
|
+
|
12
|
+
return unless promotion_code
|
13
|
+
|
14
|
+
promo_actions_ids = promotion_code.promotion.actions.pluck('spree_promotion_actions.id')
|
15
|
+
orders_ids = Spree::Adjustment
|
16
|
+
.eligible
|
17
|
+
.where(source_id: promo_actions_ids)
|
18
|
+
.pluck(:order_id)
|
19
|
+
|
20
|
+
where(id: orders_ids)
|
21
|
+
end
|
22
|
+
|
23
|
+
# List all codes used in this order
|
24
|
+
def codes_used
|
25
|
+
# Promotion actions applied in this order
|
26
|
+
promo_actions_ids = Spree::Adjustment.eligible.where(order_id: id).pluck(:source_id)
|
27
|
+
promos_ids = Spree::Promotion.joins(:promotion_actions).where('spree_promotion_actions.id IN (?)', promo_actions_ids).pluck('spree_promotions.id')
|
28
|
+
|
29
|
+
codes_used_ids = Spree::OrdersPromotion.where(promotion_id: promos_ids, order_id: id).pluck(:promotion_code_id)
|
30
|
+
Spree::PromotionCode.where(id: codes_used_ids).pluck(:code).join(', ')
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.orders_promotions_table
|
34
|
+
reflect_on_association(:orders_promotions).table_name
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Spree
|
2
|
+
class PromotionCode < Spree::Base
|
3
|
+
belongs_to :promotion
|
4
|
+
|
5
|
+
validates :code, uniqueness: true
|
6
|
+
|
7
|
+
def times_used
|
8
|
+
# Orders that have used the code
|
9
|
+
orders_ids = Spree::OrdersPromotion.where(promotion_code_id: id).pluck(:order_id)
|
10
|
+
|
11
|
+
# Promotion Actions associated with the promotion
|
12
|
+
promotion_actions_ids = promotion.actions.pluck('spree_promotion_actions.id')
|
13
|
+
|
14
|
+
# Eligible adjustments resulting of the promotion actions and only in orders
|
15
|
+
# that have used the code
|
16
|
+
Spree::Adjustment.eligible.where(source_id: promotion_actions_ids, order_id: orders_ids)
|
17
|
+
end
|
18
|
+
|
19
|
+
def times_used_count
|
20
|
+
times_used.count
|
21
|
+
end
|
22
|
+
|
23
|
+
# Orders that have used this code
|
24
|
+
def orders
|
25
|
+
order_ids = times_used.pluck(:order_id)
|
26
|
+
Spree::Order.where(id: order_ids)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Check if a code has been used less than his limit
|
30
|
+
def limit_exceeded?
|
31
|
+
usage_limit.present? && usage_limit <= times_used_count
|
32
|
+
end
|
33
|
+
|
34
|
+
class << self
|
35
|
+
def usage_limit_exceeded?(code)
|
36
|
+
return false if code.nil?
|
37
|
+
|
38
|
+
pc = find_by!(code: code.downcase)
|
39
|
+
pc.limit_exceeded?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# app/models/spree/promotion.rb
|
2
|
+
Spree::Promotion.class_eval do
|
3
|
+
has_many :promotion_codes, dependent: :destroy
|
4
|
+
alias_method :codes, :promotion_codes
|
5
|
+
|
6
|
+
has_many :order_promotions
|
7
|
+
has_many :orders, through: :order_promotions
|
8
|
+
|
9
|
+
scope :coupons, -> { where("id IN (SELECT DISTINCT(promotion_id) FROM #{promotion_codes_table})") }
|
10
|
+
scope :no_coupons, -> { where("id NOT IN (SELECT DISTINCT(promotion_id) FROM #{promotion_codes_table})") }
|
11
|
+
|
12
|
+
self.whitelisted_ransackable_attributes = %w(path promotion_category_id)
|
13
|
+
|
14
|
+
def self.promotion_codes_table
|
15
|
+
reflect_on_association(:promotion_codes).table_name
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns the promotion associated with the coupon code specified
|
19
|
+
# Doesn't return the promotion if the code is disabled
|
20
|
+
def self.with_coupon_code(coupon_code)
|
21
|
+
joins("INNER JOIN #{promotion_codes_table} ON #{promotion_codes_table}.promotion_id = #{table_name}.id")
|
22
|
+
.where("#{promotion_codes_table}.code = ? AND #{promotion_codes_table}.disabled = ?", coupon_code.strip.downcase, false)
|
23
|
+
.first
|
24
|
+
end
|
25
|
+
|
26
|
+
def activate(payload)
|
27
|
+
order = payload[:order]
|
28
|
+
return unless self.class.order_activatable?(order)
|
29
|
+
|
30
|
+
payload[:promotion] = self
|
31
|
+
|
32
|
+
# Track results from actions to see if any action has been taken.
|
33
|
+
# Actions should return nil/false if no action has been taken.
|
34
|
+
# If an action returns true, then an action has been taken.
|
35
|
+
results = actions.map do |action|
|
36
|
+
action.perform(payload)
|
37
|
+
end
|
38
|
+
# If an action has been taken, report back to whatever activated this promotion.
|
39
|
+
action_taken = results.include?(true)
|
40
|
+
|
41
|
+
if action_taken
|
42
|
+
# Connect to the order
|
43
|
+
# Create the join_table entry
|
44
|
+
# If the promotion has been unlocked thanks to a code, add the
|
45
|
+
# code to the entry
|
46
|
+
|
47
|
+
codes_downcase = codes.map { |pc| pc.code.downcase }
|
48
|
+
new_orders_promotions = Spree::OrderPromotion.new(promotion_id: id, order_id: order.id)
|
49
|
+
if order.coupon_code
|
50
|
+
# User has entered a coupon code
|
51
|
+
# Check if this promotion includes the code
|
52
|
+
if codes_downcase.include?(order.coupon_code)
|
53
|
+
new_orders_promotions.promotion_code_id = Spree::PromotionCode.find_by(code: order.coupon_code).id
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
new_orders_promotions.save
|
58
|
+
self.save
|
59
|
+
end
|
60
|
+
|
61
|
+
return action_taken
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def normalize_blank_values
|
67
|
+
%w(path).each do |column|
|
68
|
+
self[column] = nil if self[column].blank?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
Spree::PromotionHandler::Cart.class_eval do
|
2
|
+
private
|
3
|
+
|
4
|
+
def promotions
|
5
|
+
# AR cannot bind raw ASTs to prepared statements. There always must be a manager around.
|
6
|
+
# Also Postgresql requires an aliased table for `SELECT * FROM (subexpression) AS alias`.
|
7
|
+
# And Sqlite3 cannot work on outher parenthesis from `(left UNION right)`.
|
8
|
+
# So this construct makes both happy.
|
9
|
+
select = Arel::SelectManager.new(
|
10
|
+
Spree::Promotion,
|
11
|
+
Spree::Promotion.arel_table.create_table_alias(
|
12
|
+
order.promotions.active.union(Spree::Promotion.active.no_coupons.where(path: nil)),
|
13
|
+
Spree::Promotion.table_name
|
14
|
+
)
|
15
|
+
)
|
16
|
+
select.project(Arel.star)
|
17
|
+
|
18
|
+
Spree::Promotion.find_by_sql(
|
19
|
+
select,
|
20
|
+
order.promotions.bind_values
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# app/models/spree/promotion_handler/coupon_decorator.rb
|
2
|
+
Spree::PromotionHandler::Coupon.class_eval do
|
3
|
+
private
|
4
|
+
|
5
|
+
def handle_present_promotion(promotion)
|
6
|
+
return code_usage_limit_exceeded if Spree::PromotionCode.usage_limit_exceeded?(order.coupon_code)
|
7
|
+
return promotion_usage_limit_exceeded if promotion.usage_limit_exceeded?(order)
|
8
|
+
return promotion_applied if promotion_exists_on_order?(order, promotion)
|
9
|
+
unless promotion.eligible?(order)
|
10
|
+
self.error = promotion.eligibility_errors.full_messages.first unless promotion.eligibility_errors.blank?
|
11
|
+
return (self.error || ineligible_for_this_order)
|
12
|
+
end
|
13
|
+
|
14
|
+
# If any of the actions for the promotion return `true`,
|
15
|
+
# then result here will also be `true`.
|
16
|
+
result = promotion.activate(:order => order)
|
17
|
+
if result
|
18
|
+
determine_promotion_application_result
|
19
|
+
else
|
20
|
+
set_error_code :coupon_code_unknown_error
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def code_usage_limit_exceeded
|
25
|
+
set_error_code :coupon_code_max_usage
|
26
|
+
end
|
27
|
+
|
28
|
+
def determine_promotion_application_result
|
29
|
+
# Check for applied adjustments.
|
30
|
+
discount = order.all_adjustments.promotion.eligible.detect do |p|
|
31
|
+
p.source.promotion.codes.select { |pc| pc.code.try(:downcase) == order.coupon_code.downcase }.any?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Check for applied line items.
|
35
|
+
created_line_items = promotion.actions.detect { |a| a.type == 'Spree::Promotion::Actions::CreateLineItems' }
|
36
|
+
|
37
|
+
if discount || created_line_items
|
38
|
+
order.update_totals
|
39
|
+
order.persist_totals
|
40
|
+
set_success_code :coupon_code_applied
|
41
|
+
else
|
42
|
+
# if the promotion exists on an order, but wasn't found above,
|
43
|
+
# we've already selected a better promotion
|
44
|
+
if order.promotions.with_coupon_code(order.coupon_code)
|
45
|
+
set_error_code :coupon_code_better_exists
|
46
|
+
else
|
47
|
+
# if the promotion was created after the order
|
48
|
+
set_error_code :coupon_code_not_found
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|