spree-line_item_discount 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f9f715a59787c41fa9aa861b9eeb2e7b85a491cd
4
+ data.tar.gz: 6e97f1872121803605274f50b26d6ce0b0e0c089
5
+ SHA512:
6
+ metadata.gz: 8d1b8275cb2650d48a2a2f857f348f97e72afa1813efd080c74e2fe4110025028972b19f9f21eabc7fe23e7b4e862bda9d8639af03e89f1bdbac2407a19e859b
7
+ data.tar.gz: 9878ca2787faf412ced9626779bed972c1b90e9d88560789dadcadebcfd9367366821264247842630af2faba8996257d9d48d9aebafdb5b100c0c4ccb9010ca5
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ spec/dummy
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ before_script:
2
+ - sh -e /etc/init.d/xvfb start
3
+ - export DISPLAY=:99.0
4
+ - bundle update
5
+ - bundle exec rake test_app
6
+
7
+ script: bundle exec rspec spec
8
+ rvm:
9
+ - 1.9.3
10
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'spree', '~> 2.0.4'
4
+
5
+ group :test do
6
+ gem 'ffaker'
7
+ gem 'pry-rails'
8
+ end
9
+
10
+ # Specify your gem's dependencies in spree_line_item_discounts.gemspec
11
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Washington Luiz
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Spree Line Item Discounts
2
+
3
+ * Apply promo adjustments at the LineItem level on a Spree ~> 2.0 install
4
+
5
+ * Item Discount promos don't require `event_name` they should be set to nil
6
+
7
+ * Item Discount promos should only have one single action
8
+
9
+ * Adjustments should be inserted at the line item creation before the order
10
+ receive `update!`. Hopefully that will give room to make order changes way more
11
+ perfomant
12
+
13
+ * The same promo may apply adjustments to multiple line items on the order
14
+
15
+ * Multiple adjustments from different promos may be applied to the same line item
16
+
17
+ * The same adjustment cannot apply twice on the same item
18
+
19
+ ## Installation
20
+
21
+ Add to your Gemfile, bundle install and you should be ready to go.
22
+
23
+ ```ruby
24
+ gem 'spree-line_item_discount', github: 'huoxito/spree-line_item_discount'
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ 1. Create a Promotion but do not choose any event name for the promotion.
30
+ 2. Set up promotion rules as you please
31
+ 3. Add a _Create line item discount_ action to the promotion
32
+ 4. Set up the percent as you please (currently has only one Calculator)
33
+
34
+ ## Contributing
35
+
36
+ 1. Fork it
37
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
38
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
39
+ 4. Push to the branch (`git push origin my-new-feature`)
40
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/packagetask'
5
+ require 'rubygems/package_task'
6
+ require 'rspec/core/rake_task'
7
+ require 'spree/testing_support/common_rake'
8
+
9
+ Bundler::GemHelper.install_tasks
10
+ RSpec::Core::RakeTask.new
11
+
12
+ task :default => :spec
13
+
14
+ spec = eval(File.read('spree_line_item_discount.gemspec'))
15
+
16
+ Gem::PackageTask.new(spec) do |p|
17
+ p.gem_spec = spec
18
+ end
19
+
20
+ desc "Generates a dummy app for testing"
21
+ task :test_app do
22
+ ENV['LIB_NAME'] = 'spree_line_item_discount'
23
+ Rake::Task['common:test_app'].invoke
24
+ end
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ module Admin
3
+ PromotionsController.class_eval do
4
+ protected
5
+ # Overriden from Spree core
6
+ #
7
+ # Give frontend users a chance to ignore event_name as promos by the
8
+ # extension should have event_name set to nil
9
+ def load_event_names
10
+ @event_names = Spree::Activator.event_names.map { |name| [Spree.t("events.#{name}"), name] }
11
+ @event_names.unshift []
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ module Spree
2
+ Adjustment.class_eval do
3
+ scope :source_order, -> { where(source_type: "Spree::Order") }
4
+
5
+ # Overriden from Spree core
6
+ #
7
+ # Allow originator of the adjustment to perform an additional eligibility of the adjustment
8
+ # Should return _true_ if originator is absent or doesn't implement _eligible?_
9
+ #
10
+ # Pass the +adjustable+ object instead of the +source+ to +eligible?+ to allow
11
+ # the extension to check concurrent discounts for a line item. This shouldn't
12
+ # make any difference on the original mtehod for spree_core beacuse there
13
+ # both +adjustable+ and +source+ is the same Order object
14
+ #
15
+ # FIXME maybe we don't need to care about concurrent line item adjustments
16
+ # right now so passing just the order should be fine
17
+ def eligible_for_originator?
18
+ return true if originator.nil?
19
+ !originator.respond_to?(:eligible?) || originator.eligible?(adjustable)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ module Spree
2
+ LineItem.class_eval do
3
+ # Trigger LineItemDiscount promos
4
+ #
5
+ # It makes more sense to run it here because the order did not receive
6
+ # +update!+ just yet. If it complied with the usual Spree AS::Notifications
7
+ # events promo triggers we would have to run +update!+ on the order for each
8
+ # promo which is not perfomant at all
9
+ after_create :activate_discounts
10
+
11
+ private
12
+ def activate_discounts
13
+ unless order.completed?
14
+ LineItemDiscount::Adjust.all.each do |adjust_discount|
15
+ adjust_discount.perform(self)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,102 @@
1
+ module Spree
2
+ module LineItemDiscount
3
+ class Adjust < PromotionAction
4
+ include Core::CalculatedAdjustments
5
+
6
+ attr_reader :order, :line_items, :adjustable
7
+
8
+ has_many :adjustments, :as => :originator
9
+
10
+ before_validation :ensure_action_has_calculator
11
+
12
+ # decide whether adjustment should be deleted or not
13
+ before_destroy :deals_with_adjustments
14
+
15
+ # Create the adjustment
16
+ #
17
+ # It needs to create the adjustment here anyway because the action only
18
+ # performs when the line item is created.
19
+ #
20
+ # Doesn't need care about adjustment eligibility as that's done later on
21
+ # down the stack
22
+ def perform(line_item)
23
+ @order, @line_items = line_item.order, line_item
24
+
25
+ unless has_applied?(line_item)
26
+ amount = self.compute_amount(line_item)
27
+ adjustment = create_adjustment(amount: amount, adjustable: line_item, source: order)
28
+ end
29
+ end
30
+
31
+ # Receives an adjustable object (here a LineItem)
32
+ #
33
+ # FIXME Remove concurrent discount logic
34
+ def eligible?(adjustable)
35
+ @adjustable, @order = adjustable, adjustable.order
36
+ self.promotion.eligible?(order) && best_than_concurrent_discounts?
37
+ end
38
+
39
+ # Receives an adjustable object (here a LineItem)
40
+ #
41
+ # Returns total discount for the adjustable
42
+ def compute_amount(adjustable)
43
+ amount = self.calculator.compute(adjustable).to_f.abs
44
+ [adjustable.total, amount].min * -1
45
+ end
46
+
47
+ private
48
+ def has_applied?(item)
49
+ self.adjustments.map(&:adjustable).flatten.include? item
50
+ end
51
+
52
+ def create_adjustment(params)
53
+ self.adjustments.create(default_adjustment_params.merge(params), :without_protection => true)
54
+ end
55
+
56
+ def best_than_concurrent_discounts?
57
+ return false if current_discount == 0
58
+ if current_discount == best_concurrent_amount && best_concurrent_discount.eligible
59
+ return false
60
+ end
61
+
62
+ current_discount <= best_concurrent_amount || !best_concurrent_discount.eligible
63
+ end
64
+
65
+ def current_adjustment
66
+ @adjustment ||= self.adjustments.where("adjustable_id = ? AND adjustable_type = ?", adjustable.id, adjustable.class.name).first
67
+ end
68
+
69
+ def current_discount
70
+ current_adjustment.amount
71
+ end
72
+
73
+ def best_concurrent_amount
74
+ best_concurrent_discount ? best_concurrent_discount.amount : 0
75
+ end
76
+
77
+ def best_concurrent_discount
78
+ adjustable.adjustments.promotion.eligible
79
+ .where('id NOT IN (?)', [current_adjustment]).max { |a,b| a.amount.abs <=> b.amount.abs }
80
+ end
81
+
82
+ def default_adjustment_params
83
+ { :label => self.promotion.name, :mandatory => false }
84
+ end
85
+
86
+ def ensure_action_has_calculator
87
+ self.calculator = Percent.new unless self.calculator
88
+ end
89
+
90
+ def deals_with_adjustments
91
+ self.adjustments.each do |adjustment|
92
+ if adjustment.source.complete?
93
+ adjustment.originator = nil
94
+ adjustment.save
95
+ else
96
+ adjustment.destroy
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,16 @@
1
+ module Spree
2
+ module LineItemDiscount
3
+ class Percent < Calculator
4
+ preference :percent, :decimal, :default => 0
5
+ attr_accessible :preferred_percent
6
+
7
+ def self.description
8
+ Spree.t(:percent_per_item)
9
+ end
10
+
11
+ def compute(item)
12
+ ((item.price * item.quantity) * preferred_percent) / 100
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,67 @@
1
+ module Spree
2
+ module LineItemDiscount
3
+ # Compute adjustment amount and set their eligibility for all eligible promos
4
+ #
5
+ # The reason for doing it in a different class is that we only check the
6
+ # promotion eligibility once. It also should make it pretty easy to manage
7
+ # which adjustments should actually be computed. It doesn't make sense
8
+ # to compute values for adjustments that are not eligible. As the extension
9
+ # allows multiple adjustments for the same line item.
10
+ #
11
+ # e.g.
12
+ # Say we have 10 LineItemDiscount promos. An order with 5 items may have
13
+ # 50 adjustments. That would make order changes really slow as it will run
14
+ # +eligible?+ on 50 adjustments. Instead we group the data so that it
15
+ # only runs eligible? once on each promotion on each order change
16
+ #
17
+ # ps. watch out for scenarios where the promo eligibilty changes as adjustments
18
+ # are applied to the order total. Hopefully such cases don't exist and if
19
+ # they do we might just consider that adjustments or any probably other
20
+ # promotion action should not change the order eligibility among existing
21
+ # promotions
22
+ class PromotionPool
23
+ attr_reader :actions, :order
24
+
25
+ def initialize(order)
26
+ @order = order
27
+ @actions = Adjust.includes(:promotion).all
28
+ end
29
+
30
+ # Returns array of all eligible actions for the current order
31
+ def eligible
32
+ actions.select { |action| action.promotion.eligible? order }
33
+ end
34
+
35
+ # Returns array of all line item adjustments eligible for current order
36
+ def valid_discounts
37
+ adjustments(eligible.map(&:id))
38
+ end
39
+
40
+ def invalid_discounts
41
+ adjustments(not_eligible.map(&:id))
42
+ end
43
+
44
+ def not_eligible
45
+ actions.select { |action| !action.promotion.eligible? order }
46
+ end
47
+
48
+ # Update adjustment amounts for eligible actions
49
+ #
50
+ # Use +update_adjustment+ as it will not run any callbacks making
51
+ def adjust!
52
+ valid_discounts.each do |adjustment|
53
+ adjustment.originator.update_adjustment(adjustment, adjustment.adjustable)
54
+ end
55
+
56
+ valid_discounts.update_all(eligible: true)
57
+ invalid_discounts.update_all(eligible: false)
58
+ end
59
+
60
+ private
61
+ def adjustments(originator_ids)
62
+ Adjustment.promotion.source_order.includes(:originator)
63
+ .where(originator_id: originator_ids, source_id: order.id)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,44 @@
1
+ module Spree
2
+ Order.class_eval do
3
+ has_many :line_item_adjustments, through: :line_items, source: :adjustments
4
+
5
+ # Overriden from spree core
6
+ # So that we can also lock line_item adjustments
7
+ #
8
+ # TODO Refactoring spree core by adding an Order `finalize_hooks` method
9
+ # similar to what we currently have in OrderUpdater. That gives room
10
+ # to implement custom logic without having to override the +finalize!+ method
11
+ #
12
+ # Finalizes an in progress order after checkout is complete.
13
+ # Called after transition to complete state when payments will have been processed
14
+ def finalize!
15
+ touch :completed_at
16
+
17
+ # lock all adjustments (coupon promotions, etc.)
18
+ adjustments.update_all "state = 'closed'"
19
+
20
+ # As of Spree::LineItemDiscount
21
+ line_item_adjustments.update_all "state = 'closed'"
22
+
23
+ # update payment and shipment(s) states, and save
24
+ updater.update_payment_state
25
+ shipments.each do |shipment|
26
+ shipment.update!(self)
27
+ shipment.finalize!
28
+ end
29
+
30
+ updater.update_shipment_state
31
+ save
32
+ updater.run_hooks
33
+
34
+ deliver_order_confirmation_email
35
+
36
+ self.state_changes.create({
37
+ previous_state: 'cart',
38
+ next_state: 'complete',
39
+ name: 'order' ,
40
+ user_id: self.user_id
41
+ }, without_protection: true)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ module Spree
2
+ OrderUpdater.class_eval do
3
+ # Overriden from Spree core
4
+ # So that it sums line_items_adjustments total to order.adjustment_total
5
+ #
6
+ # Updates the following Order total values:
7
+ #
8
+ # +payment_total+ The total value of all finalized Payments (NOTE: non-finalized Payments are excluded)
9
+ # +item_total+ The total value of all LineItems
10
+ # +adjustment_total+ The total value of all adjustments (promotions, credits, etc.)
11
+ # +total+ The so-called "order total." This is equivalent to +item_total+ plus +adjustment_total+.
12
+ def update_totals
13
+ order.payment_total = payments.completed.map(&:amount).sum
14
+ order.item_total = line_items.map(&:amount).sum
15
+ order.adjustment_total = adjustments.eligible.map(&:amount).sum + items_adjustments_total
16
+ order.total = order.item_total + order.adjustment_total
17
+ end
18
+
19
+ # Overriden from Spree core
20
+ # As of LineItemDiscount it updates adjustments value and eligibility
21
+ #
22
+ # Updates each of the Order adjustments.
23
+ #
24
+ # This is intended to be called from an Observer so that the Order can
25
+ # respond to external changes to LineItem, Shipment, other Adjustments, etc.
26
+ #
27
+ # Adjustments will check if they are still eligible. Ineligible adjustments
28
+ # are preserved but not counted towards adjustment_total.
29
+ def update_adjustments
30
+ order.adjustments.reload.each { |adjustment| adjustment.update! }
31
+ choose_best_promotion_adjustment
32
+ LineItemDiscount::PromotionPool.new(order).adjust!
33
+ end
34
+
35
+ private
36
+ def items_adjustments_total
37
+ order.line_item_adjustments.eligible.sum(&:amount)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,12 @@
1
+ module Spree
2
+ TaxRate.class_eval do
3
+ def self.adjust(order)
4
+ order.adjustments.tax.destroy_all
5
+ order.line_item_adjustments.where(originator_type: 'Spree::TaxRate').destroy_all
6
+
7
+ self.match(order).each do |rate|
8
+ rate.adjust(order)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,22 @@
1
+ <div class="calculator-fields row">
2
+ <div class="field alpha four columns">
3
+ <% field_name = "#{param_prefix}[calculator_type]" %>
4
+ <%= label_tag field_name, t(:calculator) %>
5
+ <%= select_tag field_name, options_from_collection_for_select([Spree::LineItemDiscount::Percent], :to_s, :description, promotion_action.calculator.type),
6
+ :class => 'type-select select2 fullwidth' %>
7
+ <% if promotion_action.calculator.respond_to?(:preferences) %>
8
+ <span class="warning info"><%= t(:calculator_settings_warning) %></span>
9
+ <% end %>
10
+ </div>
11
+
12
+ <div class="settings field omega four columns">
13
+ <% promotion_action.calculator.preferences.keys.map do |key| %>
14
+ <% field_name = "#{param_prefix}[calculator_attributes][preferred_#{key}]" %>
15
+ <%= label_tag field_name, t(key.to_s) %>
16
+ <%= preference_field_tag(field_name,
17
+ promotion_action.calculator.get_preference(key),
18
+ :type => promotion_action.calculator.preference_type(key)) %>
19
+ <% end %>
20
+ <%= hidden_field_tag "#{param_prefix}[calculator_attributes][id]", promotion_action.calculator.id %>
21
+ </div>
22
+ </div>
@@ -0,0 +1,6 @@
1
+ en:
2
+ spree:
3
+ promotion_action_types:
4
+ adjust:
5
+ name: "Create line item discount"
6
+ description: "Creates an adjustment as per item on the order"
@@ -0,0 +1,2 @@
1
+ require 'spree_core'
2
+ require "spree/line_item_discount/engine"
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ module LineItemDiscount
3
+ class Engine < Rails::Engine
4
+ engine_name 'spree/line_item_discount'
5
+
6
+ config.autoload_paths += %W(#{config.root}/lib)
7
+
8
+ config.generators do |g|
9
+ g.test_framework :rspec
10
+ end
11
+
12
+ initializer 'spree.promo.register.promotions.actions' do |app|
13
+ app.config.spree.promotions.actions << Adjust
14
+ end
15
+
16
+ def self.activate
17
+ Dir.glob(File.join(File.dirname(__FILE__), "../../../app/**/*_decorator*.rb")) do |c|
18
+ Rails.configuration.cache_classes ? require(c) : load(c)
19
+ end
20
+ end
21
+
22
+ config.to_prepare &method(:activate).to_proc
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ module Spree
2
+ module LineItemDiscount
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ require 'spree/line_item_discount'
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree
4
+ module LineItemDiscount
5
+ describe Adjust do
6
+ let(:order) { create(:order) }
7
+ let(:promotion) { create(:promotion) }
8
+ let(:action) { Adjust.new }
9
+ let!(:line_item) { create(:line_item, :order => order) }
10
+
11
+ before { action.stub(:promotion => promotion) }
12
+
13
+ context "#perform" do
14
+ before { promotion.promotion_actions = [action] }
15
+
16
+ it "computes amount before creating adjustment" do
17
+ action.should_receive(:compute_amount).with(line_item).ordered
18
+ action.should_receive(:create_adjustment).ordered
19
+ action.perform(line_item)
20
+ end
21
+
22
+ it "creates adjustment with item as adjustable" do
23
+ action.perform(line_item)
24
+ line_item.adjustments.should == action.adjustments
25
+ end
26
+
27
+ it "creates adjustment with order as source" do
28
+ action.perform(line_item)
29
+ expect(line_item.adjustments.first.source).to eq order
30
+ end
31
+
32
+ it "does not perform twice on the same item" do
33
+ 2.times { action.perform(line_item) }
34
+ action.adjustments.count.should == 1
35
+ end
36
+ end
37
+
38
+ context "#eligible" do
39
+ context "promotion is not eligible" do
40
+ before { promotion.stub(eligible?: false) }
41
+ it { action.should_not be_eligible(line_item) }
42
+ end
43
+
44
+ context "has a concurrent discount" do
45
+ let(:adjustment) { mock_model(Spree::Adjustment, :amount => -100, :adjustable => line_item) }
46
+ let(:concurrent) { mock_model(Spree::Adjustment, :amount => -100, :adjustable => line_item) }
47
+
48
+ before(:each) do
49
+ action.should_receive(:current_adjustment).at_least(:once).and_return(adjustment)
50
+ action.should_receive(:best_concurrent_discount).at_least(:once).and_return(concurrent)
51
+ end
52
+
53
+ context "with same amount" do
54
+ context "but not eligible" do
55
+ before { concurrent.stub(:eligible => false) }
56
+ specify { action.should be_eligible(line_item) }
57
+ end
58
+
59
+ context "and eligible" do
60
+ before { concurrent.stub(:eligible => true) }
61
+ specify { action.should_not be_eligible(line_item) }
62
+ end
63
+ end
64
+
65
+ context "which is better" do
66
+ before { concurrent.stub(:amount => -200) }
67
+
68
+ context "but not eligible" do
69
+ before { concurrent.stub(:eligible => false) }
70
+ it { expect(action).to be_eligible(line_item) }
71
+ end
72
+
73
+ context "and eligible" do
74
+ before { concurrent.stub(:eligible => true) }
75
+ it { expect(action).to_not be_eligible(line_item) }
76
+ end
77
+ end
78
+
79
+ context "which is worse" do
80
+ before { concurrent.stub(:amount => -20) }
81
+ it { expect(action).to be_eligible(line_item) }
82
+ end
83
+ end
84
+ end
85
+
86
+ context "#compute_amount" do
87
+ before { promotion.promotion_actions = [action] }
88
+
89
+ it "calls compute on the calculator" do
90
+ action.calculator.should_receive(:compute).with(line_item)
91
+ action.compute_amount(line_item)
92
+ end
93
+
94
+ context "calculator returns amount greater than item total" do
95
+ before do
96
+ action.calculator.should_receive(:compute).with(line_item).and_return(300)
97
+ line_item.stub(total: 100)
98
+ end
99
+
100
+ it "does not exceed it" do
101
+ action.compute_amount(line_item).should eql(-100)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree
4
+ module LineItemDiscount
5
+ describe PromotionPool do
6
+ let(:order) { create(:order) }
7
+ let(:variant) { create(:variant) }
8
+
9
+ let(:rule) { Promotion::Rules::ItemTotal.create(preferred_amount: 30, preferred_operator: "gt") }
10
+ let(:calculator) { LineItemDiscount::Percent.create(preferred_percent: 10) }
11
+ let(:action) { LineItemDiscount::Adjust.new }
12
+ let(:promotion) { create(:promotion) }
13
+
14
+ before do
15
+ promotion.actions << action
16
+ action.calculator = calculator
17
+ action.save!
18
+
19
+ promotion.rules << rule
20
+ end
21
+
22
+ subject { PromotionPool.new(order) }
23
+
24
+ context "existing promo is not eligible" do
25
+ before { order.contents.add(variant, 1) }
26
+
27
+ it { expect(subject.eligible).to be_empty }
28
+
29
+ it "doesn't return adjustments from that promo" do
30
+ expect(subject.valid_discounts.map(&:originator)).to eq []
31
+ end
32
+
33
+ context "promo becomes eligible" do
34
+ let!(:previous_adjustment) { order.line_items.first.adjustments.first }
35
+
36
+ before { order.contents.add(variant, 8) }
37
+
38
+ it "adjust existing adjustments values" do
39
+ subject.adjust!
40
+ expect(subject.valid_discounts.map(&:amount)).to_not eq [previous_adjustment.amount]
41
+ end
42
+ end
43
+ end
44
+
45
+ context "existing promo is eligible" do
46
+ before { order.contents.add(variant, 8) }
47
+
48
+ it "returns array of eligible actions" do
49
+ expect(subject.eligible).to eq [action]
50
+ end
51
+
52
+ it "returns adjustments from that promo" do
53
+ expect(subject.valid_discounts.map(&:originator)).to eq [action]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree
4
+ describe LineItem do
5
+ let(:variant) { create(:variant) }
6
+ let(:order) { Order.create }
7
+
8
+ context "completed order" do
9
+ before { Order.any_instance.stub completed?: true }
10
+
11
+ it "doesnt activate discounts" do
12
+ pending "implement something like LineItemDiscount::Adjust.activate"
13
+ expect(LineItemDiscount::Adjust).not_to receive(:all)
14
+ order.contents.add(variant, 1)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree
4
+ describe Order do
5
+ let(:line_item) { create(:line_item) }
6
+ let(:order) { line_item.order }
7
+
8
+ let(:promotion) { create(:promotion) }
9
+ let(:calculator) { LineItemDiscount::Percent.create(preferred_percent: 10) }
10
+ let(:action) { LineItemDiscount::Adjust.create({promotion: promotion, calculator: calculator}, without_protection: true) }
11
+
12
+ context "finalize" do
13
+ before { action.perform(line_item) }
14
+
15
+ it "locks line item adjustments" do
16
+ order.finalize!
17
+ expect(order.line_item_adjustments.first).to be_closed
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree
4
+ describe OrderUpdater do
5
+ let(:order) { create(:order) }
6
+ let(:variant) { create(:variant) }
7
+
8
+ let(:calculator) { LineItemDiscount::Percent.create(preferred_percent: 10) }
9
+ let(:action) { LineItemDiscount::Adjust.new }
10
+ let(:promotion) { create(:promotion) }
11
+
12
+ before do
13
+ promotion.actions << action
14
+ action.calculator = calculator
15
+ action.save!
16
+ end
17
+
18
+ context "eligible promo" do
19
+ before { order.contents.add(variant, 1) }
20
+
21
+ it "applies existing line item discounts" do
22
+ expect(order.total.to_f).to_not eql variant.price.to_f
23
+ end
24
+
25
+ context "not eligible anymore" do
26
+ let(:rule) { Promotion::Rules::ItemTotal.create(preferred_amount: 3000, preferred_operator: "gt") }
27
+
28
+ before do
29
+ promotion.rules << rule
30
+ order.contents.add(variant, 1)
31
+ end
32
+
33
+ it "no longer apply line item adjustment" do
34
+ expect(order.total.to_f).to eql (variant.price * 2).to_f
35
+ end
36
+ end
37
+ end
38
+
39
+ context "promo not eligible when item is first created" do
40
+ # Order total needs to be greater than 30
41
+ let(:rule) { Promotion::Rules::ItemTotal.create(preferred_amount: 30, preferred_operator: "gt") }
42
+
43
+ before do
44
+ promotion.rules << rule
45
+ expect(variant.price).to be < rule.preferred_amount
46
+ order.contents.add(variant, 1)
47
+ end
48
+
49
+ it "doesn't apply existing discount to order total" do
50
+ expect(order.total.to_f).to eql variant.price.to_f
51
+ end
52
+
53
+ context "but becomes eligible later on" do
54
+ before do
55
+ order.contents.add(variant, 8)
56
+ end
57
+
58
+ it "applies existing discount to order total" do
59
+ expect(order.total.to_f).to_not eql (variant.price * 9).to_f
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,37 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+
3
+ require File.expand_path("../dummy/config/environment.rb", __FILE__)
4
+
5
+ require 'rspec/rails'
6
+
7
+ # Requires supporting ruby files with custom matchers and macros, etc,
8
+ # in spec/support/ and its subdirectories.
9
+ Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f }
10
+
11
+ require 'spree/testing_support/factories'
12
+
13
+ Dir["#{File.dirname(__FILE__)}/factories/**"].each do |f|
14
+ fp = File.expand_path(f)
15
+ require fp
16
+ end
17
+
18
+ RSpec.configure do |config|
19
+ # == Mock Framework
20
+ #
21
+ # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
22
+ #
23
+ # config.mock_with :mocha
24
+ # config.mock_with :flexmock
25
+ # config.mock_with :rr
26
+ config.mock_with :rspec
27
+
28
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
29
+ # config.fixture_path = "#{::Rails.root}/spec/fixtures"
30
+
31
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
32
+ # examples within a transaction, remove the following line or assign false
33
+ # instead of true.
34
+ config.use_transactional_fixtures = true
35
+
36
+ config.include FactoryGirl::Syntax::Methods
37
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'spree/line_item_discount/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "spree-line_item_discount"
8
+ gem.version = Spree::LineItemDiscount::VERSION
9
+ gem.authors = ["Washington Luiz"]
10
+ gem.email = ["huoxito@gmail.com"]
11
+ gem.description = "Apply promotion adjustments per items"
12
+ gem.summary = "Apply promotion adjustments per items"
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+
19
+ gem.add_dependency 'spree_core', '~> 2.0.0'
20
+
21
+ gem.add_development_dependency 'factory_girl', '~> 4.2'
22
+ gem.add_development_dependency 'rspec-rails', '~> 2.14'
23
+ gem.add_development_dependency 'sqlite3'
24
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spree-line_item_discount
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Washington Luiz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-08-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: spree_core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 2.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 2.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: factory_girl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '4.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '2.14'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '2.14'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Apply promotion adjustments per items
70
+ email:
71
+ - huoxito@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - .travis.yml
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - app/controllers/spree/admin/promotions_controller_decorator.rb
83
+ - app/models/spree/adjustment_decorator.rb
84
+ - app/models/spree/line_item_decorator.rb
85
+ - app/models/spree/line_item_discount/adjust.rb
86
+ - app/models/spree/line_item_discount/percent.rb
87
+ - app/models/spree/line_item_discount/promotion_pool.rb
88
+ - app/models/spree/order_decorator.rb
89
+ - app/models/spree/order_updater_decorator.rb
90
+ - app/models/spree/tax_rate_decorator.rb
91
+ - app/views/spree/admin/promotions/actions/_adjust.html.erb
92
+ - config/locales/en.yml
93
+ - lib/spree/line_item_discount.rb
94
+ - lib/spree/line_item_discount/engine.rb
95
+ - lib/spree/line_item_discount/version.rb
96
+ - lib/spree_line_item_discount.rb
97
+ - spec/models/spree/line_item_discount/adjust_spec.rb
98
+ - spec/models/spree/line_item_discount/promotion_pool_spec.rb
99
+ - spec/models/spree/line_item_spec.rb
100
+ - spec/models/spree/order_spec.rb
101
+ - spec/models/spree/order_updater_spec.rb
102
+ - spec/spec_helper.rb
103
+ - spree_line_item_discount.gemspec
104
+ homepage:
105
+ licenses: []
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - '>='
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.0.3
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: Apply promotion adjustments per items
127
+ test_files:
128
+ - spec/models/spree/line_item_discount/adjust_spec.rb
129
+ - spec/models/spree/line_item_discount/promotion_pool_spec.rb
130
+ - spec/models/spree/line_item_spec.rb
131
+ - spec/models/spree/order_spec.rb
132
+ - spec/models/spree/order_updater_spec.rb
133
+ - spec/spec_helper.rb