spree_promotion_codes 3.0.6 → 3.1.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.
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