solidus_volume_pricing 0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +35 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +8 -4
  5. data/.rubocop_todo.yml +126 -0
  6. data/CHANGELOG.md +57 -0
  7. data/Gemfile +18 -4
  8. data/Guardfile +3 -1
  9. data/README.md +53 -48
  10. data/Rakefile +2 -0
  11. data/app/assets/javascripts/spree/backend/solidus_volume_pricing.js +1 -15
  12. data/app/controllers/spree/admin/volume_price_models_controller.rb +2 -1
  13. data/app/controllers/spree/admin/volume_prices_controller.rb +3 -1
  14. data/app/decorators/controllers/solidus_volume_pricing/spree/admin/variants_controller_decorator.rb +42 -0
  15. data/app/decorators/helpers/solidus_volume_pricing/spree/base_helper_decorator.rb +31 -0
  16. data/app/decorators/models/solidus_volume_pricing/spree/line_item_decorator.rb +18 -0
  17. data/app/decorators/models/solidus_volume_pricing/spree/user_decorator.rb +17 -0
  18. data/app/decorators/models/solidus_volume_pricing/spree/variant_decorator.rb +21 -0
  19. data/app/models/solidus_volume_pricing/price_display.rb +47 -0
  20. data/app/models/solidus_volume_pricing/pricer.rb +95 -0
  21. data/app/models/solidus_volume_pricing/pricing_options.rb +20 -0
  22. data/app/models/spree/volume_price.rb +43 -26
  23. data/app/models/spree/volume_price_model.rb +6 -4
  24. data/app/overrides/spree/admin/shared/_settings_sub_menu/add_volume_price_model_admin_menu_links.html.erb.deface +5 -0
  25. data/app/overrides/views_decorator.rb +2 -0
  26. data/app/views/spree/admin/shared/_vp_product_tab.html.erb +2 -2
  27. data/app/views/spree/admin/variants/_edit_fields.html.erb +5 -32
  28. data/app/views/spree/admin/variants/volume_prices.html.erb +9 -36
  29. data/app/views/spree/admin/volume_price_models/_form.html.erb +6 -2
  30. data/app/views/spree/admin/volume_price_models/_list.html.erb +8 -4
  31. data/app/views/spree/admin/volume_price_models/_select.html.erb +17 -0
  32. data/app/views/spree/admin/volume_price_models/edit.html.erb +7 -6
  33. data/app/views/spree/admin/volume_price_models/index.html.erb +7 -5
  34. data/app/views/spree/admin/volume_price_models/new.html.erb +5 -5
  35. data/app/views/spree/admin/volume_prices/_table.html.erb +26 -0
  36. data/app/views/spree/admin/volume_prices/_volume_price_fields.html.erb +6 -6
  37. data/app/views/spree/products/_volume_pricing.html.erb +8 -8
  38. data/bin/rails +3 -2
  39. data/config/locales/de.yml +17 -3
  40. data/config/locales/en.yml +21 -2
  41. data/config/locales/pt.yml +6 -2
  42. data/config/locales/ru.yml +6 -2
  43. data/config/locales/sv.yml +6 -2
  44. data/config/locales/tr.yml +6 -2
  45. data/config/routes.rb +3 -1
  46. data/db/migrate/20081119145604_create_volume_prices.rb +3 -1
  47. data/db/migrate/20110203174010_change_display_name_for_volume_prices.rb +3 -1
  48. data/db/migrate/20111206173307_prefix_volume_pricing_table_names.rb +3 -1
  49. data/db/migrate/20121115043422_add_discount_type_column.rb +3 -1
  50. data/db/migrate/20150513200904_add_role_to_volume_price.rb +3 -1
  51. data/db/migrate/20150603143015_create_spree_volume_price_models.rb +3 -1
  52. data/lib/generators/solidus_volume_pricing/install/install_generator.rb +3 -5
  53. data/lib/solidus_volume_pricing.rb +5 -1
  54. data/lib/solidus_volume_pricing/engine.rb +5 -22
  55. data/lib/solidus_volume_pricing/range_from_string.rb +36 -0
  56. data/lib/solidus_volume_pricing/version.rb +5 -3
  57. data/solidus_volume_pricing.gemspec +6 -14
  58. data/spec/controllers/spree/admin/variants_controller_spec.rb +18 -15
  59. data/spec/factories/volume_price_factory.rb +7 -5
  60. data/spec/features/manage_volume_price_models_feature_spec.rb +5 -3
  61. data/spec/features/manage_volume_prices_feature_spec.rb +6 -4
  62. data/spec/helpers/base_helper_spec.rb +3 -1
  63. data/spec/lib/solidus_volume_pricing/range_from_string_spec.rb +61 -0
  64. data/spec/models/solidus_volume_pricing/pricer_spec.rb +673 -0
  65. data/spec/models/solidus_volume_pricing/pricing_options_spec.rb +57 -0
  66. data/spec/models/spree/line_item_spec.rb +24 -21
  67. data/spec/models/spree/order_spec.rb +4 -2
  68. data/spec/models/spree/variant_spec.rb +3 -303
  69. data/spec/models/spree/volume_price_spec.rb +153 -49
  70. data/spec/spec_helper.rb +5 -38
  71. data/spec/support/shoulda.rb +11 -0
  72. metadata +37 -173
  73. data/.hound.yml +0 -40
  74. data/.travis.yml +0 -12
  75. data/CONTRIBUTING.md +0 -81
  76. data/app/controllers/spree/admin/variants_controller_decorator.rb +0 -32
  77. data/app/helpers/spree/base_helper_decorator.rb +0 -19
  78. data/app/models/spree/line_item_decorator.rb +0 -13
  79. data/app/models/spree/user_decorator.rb +0 -10
  80. data/app/models/spree/variant_decorator.rb +0 -104
  81. data/app/overrides/spree/admin/shared/sub_menu/_configuration/add_volume_price_model_admin_menu_links.html.erb.deface +0 -3
  82. data/app/views/spree/admin/volume_prices/_edit_fields.html.erb +0 -31
  83. data/spec/support/capybara.rb +0 -12
  84. data/spec/support/database_cleaner.rb +0 -21
  85. data/spec/support/factory_girl.rb +0 -7
  86. data/spec/support/spree.rb +0 -10
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler'
2
4
  Bundler::GemHelper.install_tasks
3
5
 
@@ -1,16 +1,2 @@
1
- //= require spree/backend
1
+ // Just here for backwards compatibility
2
2
 
3
- // spree's version only handles 'input', not 'select', and this breaks solidus_volume_pricing
4
-
5
- $(function () {
6
- $('#add_volume_price').click( function() {
7
- var target = $(this).data("target"),
8
- new_table_row = $(target + ' tr:visible:first');
9
- new_table_row.find('div.select2').remove();
10
- $('select.select2').select2({
11
- allowClear: true,
12
- dropdownAutoWidth: true
13
- });
14
- });
15
-
16
- });
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Spree
2
4
  module Admin
3
5
  class VolumePriceModelsController < ResourceController
4
-
5
6
  before_action :load_volume_prices, only: [:new, :edit]
6
7
 
7
8
  private
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Spree
2
4
  module Admin
3
5
  class VolumePricesController < Spree::Admin::BaseController
4
6
  def destroy
5
7
  @volume_price = Spree::VolumePrice.find(params[:id])
6
8
  @volume_price.destroy
7
- render nothing: true
9
+ head :ok
8
10
  end
9
11
  end
10
12
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusVolumePricing
4
+ module Spree
5
+ module Admin
6
+ module VariantsControllerDecorator
7
+ def edit
8
+ @variant.volume_prices.build if @variant.volume_prices.empty?
9
+ super
10
+ end
11
+
12
+ def volume_prices
13
+ @product = @variant.product
14
+ @variant.volume_prices.build if @variant.volume_prices.empty?
15
+ end
16
+
17
+ private
18
+
19
+ # this loads the variant for the master variant volume price editing
20
+ def load_resource_instance
21
+ parent
22
+
23
+ if new_actions.include?(params[:action].to_sym)
24
+ build_resource
25
+ elsif params[:id]
26
+ ::Spree::Variant.find(params[:id])
27
+ end
28
+ end
29
+
30
+ def location_after_save
31
+ if @product.master.id == @variant.id && params[:variant].key?(:volume_prices_attributes)
32
+ return volume_prices_admin_product_variant_url(@product, @variant)
33
+ end
34
+
35
+ super
36
+ end
37
+
38
+ ::Spree::Admin::VariantsController.prepend self
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusVolumePricing
4
+ module Spree
5
+ module BaseHelperDecorator
6
+ def self.prepended(base)
7
+ base.module_eval do
8
+ def display_volume_price(variant, quantity = 1, user = nil)
9
+ price_display(variant, quantity: quantity, user: user).price_string
10
+ end
11
+
12
+ def display_volume_price_earning_percent(variant, quantity = 1, user = nil)
13
+ price_display(variant, quantity: quantity, user: user).earning_percent_string
14
+ end
15
+
16
+ def display_volume_price_earning_amount(variant, quantity = 1, user = nil)
17
+ price_display(variant, quantity: quantity, user: user).earning_amount_string
18
+ end
19
+
20
+ private
21
+
22
+ def price_display(variant, quantity:, user:)
23
+ SolidusVolumePricing::PriceDisplay.new(variant, quantity: quantity, user: user)
24
+ end
25
+ end
26
+ end
27
+
28
+ ::Spree::BaseHelper.prepend self
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusVolumePricing
4
+ module Spree
5
+ module LineItemDecorator
6
+ def set_pricing_attributes
7
+ if quantity_changed?
8
+ options = SolidusVolumePricing::PricingOptions.from_line_item(self)
9
+ self.money_price = SolidusVolumePricing::Pricer.new(variant).price_for(options)
10
+ end
11
+
12
+ super
13
+ end
14
+
15
+ ::Spree::LineItem.prepend self
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusVolumePricing
4
+ module Spree
5
+ module UserDecorator
6
+ def resolve_role
7
+ if has_spree_role? ::Spree::Config.volume_pricing_role.to_sym
8
+ ::Spree::Role.find_by name: ::Spree::Config.volume_pricing_role
9
+ else
10
+ ::Spree::Role.find_by name: 'user'
11
+ end
12
+ end
13
+
14
+ ::Spree.user_class.prepend self
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusVolumePricing
4
+ module Spree
5
+ module VariantDecorator
6
+ def self.prepended(base)
7
+ base.class_eval do
8
+ has_and_belongs_to_many :volume_price_models
9
+ has_many :volume_prices, -> { order(position: :asc) }, dependent: :destroy
10
+ has_many :model_volume_prices, -> { order(position: :asc) }, class_name: '::Spree::VolumePrice', through: :volume_price_models, source: :volume_prices
11
+ accepts_nested_attributes_for :volume_prices, allow_destroy: true,
12
+ reject_if: proc { |volume_price|
13
+ volume_price[:amount].blank? && volume_price[:range].blank?
14
+ }
15
+ end
16
+ end
17
+
18
+ ::Spree::Variant.prepend self
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusVolumePricing
4
+ class PriceDisplay
5
+ attr_reader :variant, :quantity, :user
6
+
7
+ def initialize(variant, quantity: 1, user: nil)
8
+ @variant = variant
9
+ @quantity = quantity
10
+ @user = user
11
+ end
12
+
13
+ def price_string
14
+ price.to_s
15
+ end
16
+
17
+ def earning_amount_string
18
+ earning_amount.to_s
19
+ end
20
+
21
+ def earning_percent_string
22
+ earning_percent.to_s
23
+ end
24
+
25
+ private
26
+
27
+ def price
28
+ pricer.price_for(options)
29
+ end
30
+
31
+ def earning_amount
32
+ pricer.earning_amount(options)
33
+ end
34
+
35
+ def earning_percent
36
+ pricer.earning_percent(options)
37
+ end
38
+
39
+ def options
40
+ SolidusVolumePricing::PricingOptions.new(quantity: quantity, user: user)
41
+ end
42
+
43
+ def pricer
44
+ SolidusVolumePricing::Pricer.new(variant)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusVolumePricing
4
+ class Pricer < ::Spree::Variant::PriceSelector
5
+ attr_reader :quantity, :user
6
+
7
+ def self.pricing_options_class
8
+ SolidusVolumePricing::PricingOptions
9
+ end
10
+
11
+ def price_for(pricing_options)
12
+ extract_options(pricing_options)
13
+ ::Spree::Money.new(computed_price)
14
+ end
15
+
16
+ def earning_amount(pricing_options)
17
+ extract_options(pricing_options)
18
+ ::Spree::Money.new(computed_earning)
19
+ end
20
+
21
+ def earning_percent(pricing_options)
22
+ extract_options(pricing_options)
23
+ computed_earning_percent.round
24
+ end
25
+
26
+ private
27
+
28
+ def extract_options(pricing_options)
29
+ @quantity = pricing_options.quantity
30
+ @user = pricing_options.user
31
+ end
32
+
33
+ def use_master_variant_volume_pricing?
34
+ ::Spree::Config.use_master_variant_volume_pricing && @variant.volume_prices.empty?
35
+ end
36
+
37
+ def variant
38
+ if use_master_variant_volume_pricing?
39
+ super.product.master
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ def volume_prices
46
+ ::Spree::VolumePrice.for_variant(variant, user: user)
47
+ end
48
+
49
+ def volume_price
50
+ volume_prices.detect do |volume_price|
51
+ volume_price.include?(quantity)
52
+ end
53
+ end
54
+
55
+ def computed_price
56
+ case volume_price&.discount_type
57
+ when 'price'
58
+ volume_price.amount
59
+ when 'dollar'
60
+ variant.price - volume_price.amount
61
+ when 'percent'
62
+ variant.price * (1 - volume_price.amount)
63
+ else
64
+ variant.price
65
+ end
66
+ end
67
+
68
+ def computed_earning
69
+ case volume_price&.discount_type
70
+ when 'price'
71
+ variant.price - volume_price.amount
72
+ when 'dollar'
73
+ volume_price.amount
74
+ when 'percent'
75
+ variant.price - (variant.price * (1 - volume_price.amount))
76
+ else
77
+ 0
78
+ end
79
+ end
80
+
81
+ def computed_earning_percent
82
+ case volume_price&.discount_type
83
+ when 'price'
84
+ diff = variant.price - volume_price.amount
85
+ diff * 100 / variant.price
86
+ when 'dollar'
87
+ volume_price.amount * 100 / variant.price
88
+ when 'percent'
89
+ volume_price.amount * 100
90
+ else
91
+ 0
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusVolumePricing
4
+ class PricingOptions < ::Spree::Variant::PricingOptions
5
+ attr_accessor :quantity, :user
6
+
7
+ def initialize(options = {})
8
+ super options.except(:quantity, :user)
9
+ @quantity = options.delete(:quantity)
10
+ @user = options.delete(:user)
11
+ end
12
+
13
+ def self.from_line_item(line_item)
14
+ pricing_options = super(line_item)
15
+ pricing_options.quantity = line_item.quantity
16
+ pricing_options.user = line_item.order.user
17
+ pricing_options
18
+ end
19
+ end
20
+ end
@@ -1,35 +1,52 @@
1
- class Spree::VolumePrice < ActiveRecord::Base
2
- belongs_to :variant, touch: true
3
- belongs_to :volume_price_model, touch: true
4
- belongs_to :spree_role, class_name: 'Spree::Role', foreign_key: 'role_id'
1
+ # frozen_string_literal: true
2
+
3
+ class Spree::VolumePrice < ApplicationRecord
4
+ belongs_to :variant, touch: true, optional: true
5
+ belongs_to :volume_price_model, touch: true, optional: true
6
+ belongs_to :spree_role, class_name: 'Spree::Role', foreign_key: 'role_id', optional: true
5
7
  acts_as_list scope: [:variant_id, :volume_price_model_id]
6
8
 
7
9
  validates :amount, presence: true
8
10
  validates :discount_type,
9
- presence: true,
10
- inclusion: {
11
- in: %w(price dollar percent),
12
- message: I18n.t(:'activerecord.errors.messages.is_not_a_valid_volume_price_type', value: self)
13
- }
14
- validates :range,
15
- format: {
16
- with: /\(?[0-9]+(?:\.{2,3}[0-9]+|\+\)?)/,
17
- message: I18n.t(:'activerecord.errors.messages.must_be_in_format')
18
- }
19
-
20
- OPEN_ENDED = /\(?[0-9]+\+\)?/
21
-
22
- def include?(quantity)
23
- if open_ended?
24
- bound = /\d+/.match(range)[0].to_i
25
- return quantity >= bound
26
- else
27
- range.to_range === quantity
11
+ presence: true,
12
+ inclusion: {
13
+ in: %w(price dollar percent)
14
+ }
15
+
16
+ validate :range_format
17
+
18
+ def self.for_variant(variant, user: nil)
19
+ roles = [nil]
20
+ if user
21
+ roles << user.resolve_role&.id
22
+ end
23
+
24
+ where(
25
+ arel_table[:variant_id].eq(variant.id).
26
+ or(
27
+ arel_table[:volume_price_model_id].in(variant.volume_price_model_ids)
28
+ )
29
+ ).
30
+ where(role_id: roles).
31
+ order(position: :asc, amount: :asc)
32
+ end
33
+
34
+ delegate :include?, to: :range_from_string
35
+
36
+ def display_range
37
+ range.gsub(/\.+/, "-").gsub(/\(|\)/, '')
38
+ end
39
+
40
+ private
41
+
42
+ def range_format
43
+ if !(SolidusVolumePricing::RangeFromString::RANGE_FORMAT =~ range ||
44
+ SolidusVolumePricing::RangeFromString::OPEN_ENDED =~ range)
45
+ errors.add(:range, :must_be_in_format)
28
46
  end
29
47
  end
30
48
 
31
- # indicates whether or not the range is a true Ruby range or an open ended range with no upper bound
32
- def open_ended?
33
- OPEN_ENDED =~ range
49
+ def range_from_string
50
+ SolidusVolumePricing::RangeFromString.new(range).to_range
34
51
  end
35
52
  end
@@ -1,8 +1,10 @@
1
- class Spree::VolumePriceModel < ActiveRecord::Base
1
+ # frozen_string_literal: true
2
+
3
+ class Spree::VolumePriceModel < ApplicationRecord
2
4
  has_many :variants
3
5
  has_many :volume_prices, -> { order(position: :asc) }, dependent: :destroy
4
6
  accepts_nested_attributes_for :volume_prices, allow_destroy: true,
5
- reject_if: proc { |volume_price|
6
- volume_price[:amount].blank? && volume_price[:range].blank?
7
- }
7
+ reject_if: proc { |volume_price|
8
+ volume_price[:amount].blank? && volume_price[:range].blank?
9
+ }
8
10
  end