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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +24 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE +26 -0
  7. data/README.md +42 -0
  8. data/Rakefile +21 -0
  9. data/app/controllers/spree/admin/promotion_codes_controller.rb +7 -0
  10. data/app/helpers/spree/admin/promotion_codes_helper.rb +13 -0
  11. data/app/models/spree.rb +5 -0
  12. data/app/models/spree/order_decorator.rb +36 -0
  13. data/app/models/spree/order_promotion.rb +4 -0
  14. data/app/models/spree/promotion_code.rb +43 -0
  15. data/app/models/spree/promotion_decorator.rb +71 -0
  16. data/app/models/spree/promotion_handler/cart_decorator.rb +24 -0
  17. data/app/models/spree/promotion_handler/coupon_decorator.rb +52 -0
  18. data/app/models/spree/promotion_handler/free_shipping_decorator.rb +9 -0
  19. data/app/models/spree/user_decorator.rb +3 -0
  20. data/app/overrides/orders/edit_override.rb +4 -0
  21. data/app/overrides/orders/index_override.rb +4 -0
  22. data/app/overrides/promotions/index_override.rb +16 -0
  23. data/app/overrides/promotions/manage_codes.rb +10 -0
  24. data/app/views/spree/admin/promotion_codes/_form.html.erb +29 -0
  25. data/app/views/spree/admin/promotion_codes/edit.html.erb +12 -0
  26. data/app/views/spree/admin/promotion_codes/index.html.erb +41 -0
  27. data/app/views/spree/admin/promotion_codes/new.html.erb +12 -0
  28. data/app/views/spree/admin/shared/_edit_orders.html.erb +17 -0
  29. data/app/views/spree/admin/shared/_index_orders_search.html.erb +8 -0
  30. data/app/views/spree/admin/shared/_index_promotions_table.html.erb +27 -0
  31. data/bin/rails +7 -0
  32. data/config/locales/en.yml +15 -0
  33. data/config/locales/es.yml +15 -0
  34. data/config/routes.rb +7 -0
  35. data/db/migrate/20161027141141_create_spree_promotion_codes.rb +10 -0
  36. data/db/migrate/20161028123452_add_disabled_to_promotion_codes.rb +5 -0
  37. data/db/migrate/20161101192534_move_code_to_promotion_code.rb +9 -0
  38. data/db/migrate/20161101192804_remove_code_from_promotion.rb +5 -0
  39. data/db/migrate/20161103111824_add_missing_indexes_to_promotion_codes.rb +6 -0
  40. data/db/migrate/20161103112910_add_promotion_code_to_orders_promotion.rb +6 -0
  41. data/db/migrate/20161104092718_add_usage_limit_to_promotion_code.rb +5 -0
  42. data/lib/generators/spree_promotion_codes/install/install_generator.rb +21 -0
  43. data/lib/spree_promotion_codes.rb +2 -0
  44. data/lib/spree_promotion_codes/engine.rb +20 -0
  45. data/lib/spree_promotion_codes/factories.rb +6 -0
  46. data/spec/models/spree/promotion_decorator_spec.rb +35 -0
  47. data/spec/models/spree/promotion_handler/coupon_decorator_spec.rb +325 -0
  48. data/spec/rails_helper.rb +57 -0
  49. data/spec/spec_helper.rb +94 -0
  50. data/spree_promotion_codes.gemspec +33 -0
  51. metadata +57 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 515907c22ba20ab1d5961f3643ac22ec80d11057
4
- data.tar.gz: d96150a83bc378294d26a69776deae6bb6386223
3
+ metadata.gz: 04239c8ac7e88097d26826b991038becf9f4da69
4
+ data.tar.gz: 13c944c77025b4c659ddea190e020a1f06ce94ca
5
5
  SHA512:
6
- metadata.gz: a387ce046a76aac1095be3ece2442aa4d9007a9f1412de0df1120f343a600992e8764b7ce1b7cf7a36f337241481de904692c27051e1c90b37f0ef63328c5216
7
- data.tar.gz: d76a4bf7843640154ca58ba4d3b6e97ad8049aefe1394d2b75e9e721382d610c94d37c685b7a60e3dce134c3a4ef312a7888c0b601bd8ce3864df586498760a9
6
+ metadata.gz: cb328a7dd93a8e395c34a95fe826c4920e82b134032a2ff6538b10369edea3b2722c67293d032ca16d925b285d6bff1ce116a6208cc5fdcb3ad0574e259cdf4c
7
+ data.tar.gz: 2954ba07f253937f8763ae5a5ad0b07fab490bacd8da28c05e6e5eb87deec44654de32a161d59545a0d4c696a6bf38637c5532476b9e7ac3001c72729c0e80ac
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ \#*
2
+ *~
3
+ .#*
4
+ .DS_Store
5
+ .idea
6
+ .project
7
+ .sass-cache
8
+ coverage
9
+ Gemfile.lock
10
+ tmp
11
+ nbproject
12
+ pkg
13
+ *.swp
14
+ spec/dummy
15
+ .bundle
16
+ .rvmrc
17
+ .ruby-version
18
+ .ruby-gemset
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
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
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'spree', github: 'spree/spree', branch: '3-1-stable'
4
+ gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '3-1-stable'
5
+
6
+ gemspec
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
@@ -0,0 +1,7 @@
1
+ module Spree
2
+ module Admin
3
+ class PromotionCodesController < ResourceController
4
+ belongs_to 'spree/promotion'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module Spree
2
+ module Admin
3
+ module PromotionCodesHelper
4
+ def humanize_disabled(disabled)
5
+ if disabled
6
+ Spree.t('promotion_code.disabled')
7
+ else
8
+ Spree.t('promotion_code.not_disabled')
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module Spree
2
+ def self.table_name_prefix
3
+ 'spree_'
4
+ end
5
+ end
@@ -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,4 @@
1
+ class Spree::OrderPromotion < Spree::Base
2
+ belongs_to :order
3
+ belongs_to :promotion
4
+ 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
@@ -0,0 +1,9 @@
1
+ Spree::PromotionHandler::FreeShipping.class_eval do
2
+ def activate
3
+ promotions.each do |promotion|
4
+ next if promotion.codes.any? && !order_promo_ids.include?(promotion.id)
5
+
6
+ promotion.activate(order: order) if promotion.eligible?(order)
7
+ end
8
+ end
9
+ end