Skip to content
Browse files

Add progressive volume discount strategy.

  • Loading branch information...
1 parent d3b2f84 commit 991a7868e3a0711b2c95b3b166216c291b169e97 @amw amw committed Mar 3, 2011
View
113 README.md
@@ -1,6 +1,5 @@
Simple Volume Pricing
=====================
-
Simple Volume Pricing is an extension to Spree (a complete open source commerce
solution for Ruby on Rails) that allows order quantity to determine the price
for a particular product variant. For instance the variant's starting price
@@ -17,8 +16,27 @@ Each VolumePrice contains the following values:
3. **Price:** The price of the variant if the line item quantity is big enough
for this VolumePrice to apply.
-Examples
-========
+Additionally Variant objects get a new boolean property
+`progressive_volume_discount` to select between two available discount
+strategies.
+
+
+Uniform vs Progressive volume discount
+======================================
+This extension supports two discount strategies. Uniform volume discount selects
+one VolumePrice based on ordered quantity and applies it to all ordered units.
+Progressive volume discount applies different VolumePrices to different portions
+of the item's quantity. This means that you can charge i.e. $15 for the first
+three units in the cart, $13 for the next five and $10 for all additional.
+
+Some people find progressive volume discount easier to configure. With prices
+applied uniformly your customers often end up in situations when it's cheaper to
+buy X + Y units than just X (a substantial price drop can neglect an added
+quantity).
+
+
+Uniform Volume Discount examples
+================================
Rails T-Shirt variant has a price of $19.99. Consider the following examples of
volume prices:
@@ -28,7 +46,6 @@ volume prices:
Rails T-Shirt 20 15.00
## Example 1
-
Cart Contents:
Product Quantity Price Total
@@ -37,11 +54,9 @@ Cart Contents:
Order details:
- Volume Discount: 0.00
Subtotal: 19.99
## Example 2
-
Cart Contents:
Product Quantity Price Total
@@ -50,40 +65,65 @@ Cart Contents:
Order details:
- Volume Discount: -9.95 # 5 * (19.99 - 18)
- Subtotal: 90.00
+ Volume Discount: -9.95 # 5 * (19.99 - 18.00)
+ Subtotal: 90.00 # 5 * 18.00
## Example 3
-
Cart Contents:
- Product Quantity Price Total
- ----------------------------------------------------------------
- Rails T-Shirt 6 19.99 119.94
+ Product Quantity Price Total
+ ----------------------------------------------------------------
+ Rails T-Shirt 6 19.99 119.94
Order details:
- Volume Discount: -11.94 # 6 * (19.99 - 18)
- Subtotal: 108.00
+ Volume Discount: -11.94 # 6 * (19.99 - 18.00)
+ Subtotal: 108.00 # 6 * 18.00
## Example 4
+Cart Contents:
+
+ Product Quantity Price Total
+ ----------------------------------------------------------------
+ Rails T-Shirt 20 19.99 399.80
+
+Order details:
+
+ Volume Discount: -99.80 # 20 * (19.99 - 15.00)
+ Subtotal: 300.00 # 20 * 15.00
+
+Progressive Volume Discount examples
+====================================
+Given the same volume prices configuration as in uniform discount examples.
+
+## Example 1
Cart Contents:
- Product Quantity Price Total
- ----------------------------------------------------------------
- Rails T-Shirt 20 19.99 399.80
+ Product Quantity Price Total
+ ----------------------------------------------------------------
+ Rails T-Shirt 6 19.99 119.94
Order details:
- Volume Discount: -99.80 # 20 * (19.99 - 15)
- Subtotal: 300.00
+ Volume Discount: -3.98
+ Subtotal: 115.96 # 4 * 19.99 + 2 * 18.00
+
+## Example 2
+Cart Contents:
+
+ Product Quantity Price Total
+ ----------------------------------------------------------------
+ Rails T-Shirt 25 19.99 499.75
+
+Order details:
+ Volume Discount: -59.79
+ Subtotal: 439.96 # 4 * 19.99 + 15 * 18.00 + 6 * 15.00
Why is it simple
================
-
This extension is called Simple to differentiate it from [another volume pricing
extension](https://github.com/railsdog/spree-volume-pricing) created and
maintained by the Spree Core team at RailsDog.
@@ -110,7 +150,6 @@ customer's past orders in volume price calculation you can overwrite
`Order::variant_starting_quantity(variant)` method. By default it returns 0.
## Example
-
If you want to calculate customer's volume discount based on his order history
from last 31 days just add this to your site's code:
@@ -123,41 +162,33 @@ from last 31 days just add this to your site's code:
end
end
-Assuming the same volume prices configuration as above. First order:
+Assuming the same volume prices configuration as above and uniform volume
+discount strategy. First order:
- Product Quantity Price Total
- ----------------------------------------------------------------
- Rails T-Shirt 8 19.99 159.92
+ Product Quantity Price Total
+ ----------------------------------------------------------------
+ Rails T-Shirt 8 19.99 159.92
Order details:
- Volume Discount: -15.92 # 8 * (19.99 - 18)
- Subtotal: 144.00
+ Volume Discount: -15.92 # 8 * (19.99 - 18.00)
+ Subtotal: 144.00 # 8 * 18.00
Next order during the next 31 days:
- Product Quantity Price Total
- ----------------------------------------------------------------
- Rails T-Shirt 4 19.99 79.96
+ Product Quantity Price Total
+ ----------------------------------------------------------------
+ Rails T-Shirt 4 19.99 79.96
Order details:
- Volume Discount: -7.96 # 4 * (19.99 - 18)
- Subtotal: 72.00
-
+ Volume Discount: -7.96 # 4 * (19.99 - 18.00)
+ Subtotal: 72.00 # 4 * 18.00
-Additional Notes
-================
-
-* The volume discount is calculated by applying the discount price to all
- ordered units of a particular variant. It does not (yet) apply different
- prices for the portion of the quantity that falls within a particular range.
- Although I plan to support such option.
Authors
=======
-
This extension is based on
[spree-volume-pricing](https://github.com/railsdog/spree-volume-pricing)
extension. It was rewritten by Adam Wróbel of Flux Inc, but there are some bits
View
48 app/models/line_item_decorator.rb
@@ -7,15 +7,13 @@ def update_volume_discount updated_order = nil
return if self.quantity < 1 || variant.volume_prices.empty?
self.order = updated_order if updated_order
- total_quantity = self.quantity + order.variant_starting_quantity(variant)
- final_price = default_price = self.price
+ starting_quantity = order.variant_starting_quantity(variant)
- variant.volume_prices.each do |vp|
- break if vp.starting_quantity > total_quantity
- final_price = vp.price
+ self.volume_discount = if variant.progressive_volume_discount
+ progressive_price_strategy starting_quantity
+ else
+ uniform_price_strategy starting_quantity
end
-
- self.volume_discount = self.quantity * (final_price - default_price)
end
def amount_with_volume_discount
@@ -28,4 +26,40 @@ def amount_with_volume_discount
def check_update_volume_discount
update_volume_discount if price_changed? || quantity_changed?
end
+
+ def uniform_price_strategy starting_quantity
+ total_quantity = self.quantity + starting_quantity
+ final_price = default_price = self.price
+
+ variant.volume_prices.each do |vp|
+ break if vp.starting_quantity > total_quantity
+ final_price = vp.price
+ end
+
+ self.quantity * (final_price - default_price)
+ end
+
+ def progressive_price_strategy units_processed
+ discount = 0
+ total_quantity = self.quantity + units_processed
+ current_price = default_price = self.price
+
+ variant.volume_prices.each do |vp|
+ if vp.starting_quantity - 1 > units_processed
+ last_unit_for_this_price = [total_quantity, vp.starting_quantity - 1].min
+ items_count = last_unit_for_this_price - units_processed
+ discount += items_count * (current_price - default_price)
+ units_processed = last_unit_for_this_price
+ end
+ break if vp.starting_quantity > total_quantity
+ current_price = vp.price
+ end
+
+ if total_quantity > units_processed
+ items_count = total_quantity - units_processed
+ discount += items_count * (current_price - default_price)
+ end
+
+ discount
+ end
end
View
4 app/models/product_decorator.rb
@@ -1,5 +1,7 @@
Product.class_eval do
- delegate_belongs_to :master, :volume_prices_attributes=
+ delegate_belongs_to :master,
+ :volume_prices_attributes=,
+ :progressive_volume_discount
def save_master
return unless master && (master.changed? || master.new_record? || master.changed_for_autosave?)
View
1 app/models/variant_decorator.rb
@@ -16,6 +16,7 @@ def blank_volume_price attributes
private
def copy_master_volume_prices
return if self.is_master?
+ self.progressive_volume_discount = self.product.master.progressive_volume_discount
self.volume_prices = self.product.master.volume_prices.map do |vp|
VolumePrice.new vp.attributes.slice('starting_quantity', 'price')
end
View
14 app/views/admin/products/volume_prices.html.erb
@@ -7,6 +7,20 @@
<%= form_for 'product', @product.master, :url => object_url,
:html => { :method => :put } do |f| %>
<h3><%= t("volume_prices") %></h3>
+ <style>
+ #progressive_discount_info {
+ display: inline-block;
+ margin-left: 20pt;
+ font-size: 0.8em;
+ }
+ </style>
+ <p>
+ <%= f.check_box :progressive_volume_discount %>
+ <%= f.label :progressive_volume_discount %>
+ <span id="progressive_discount_info">
+ <%= t 'progressive_discount_info' %>
+ </span>
+ </p>
<table class="index">
<thead>
<tr>
View
14 app/views/admin/variants/_volume_prices.html.erb
@@ -1,5 +1,19 @@
<%= fields_for @variant do |f| %>
<h3><%= t("volume_prices") %></h3>
+ <style>
+ #progressive_discount_info {
+ display: inline-block;
+ margin-left: 20pt;
+ font-size: 0.8em;
+ }
+ </style>
+ <p>
+ <%= f.check_box :progressive_volume_discount %>
+ <%= f.label :progressive_volume_discount %>
+ <span id="progressive_discount_info">
+ <%= t 'progressive_discount_info' %>
+ </span>
+ </p>
<table class="index">
<thead>
<tr>
View
75 app/views/products/_volume_prices.html.erb
@@ -1,22 +1,55 @@
-<p class="prices">
- <%= t("price") -%>
- <br />
- <span class="price selling"><%= product_price(@product) %></span><br />
-</p>
-<% if !@product.master.volume_prices.empty? %>
- <% if @product.has_variants? %>
- <p class="volume_prices">
- Volume prices available.
- </p>
- <% else %>
- <p class="volume_prices">
- Volume prices available:<br />
- <span style="font-size: 0.8em;">
- <% @product.master.volume_prices.each do |vp| -%>
- <span class="price selling"><%= product_price(vp) %></span>
- when buying <%= vp.starting_quantity %> or more<br />
- <% end %>
- </span>
- </p>
- <% end %>
+<% if @product.master.volume_prices.empty? %>
+ <p class="prices">
+ <%= t("price") -%>
+ <br />
+ <span class="price selling"><%= product_price(@product) %></span><br />
+ </p>
+<% elsif @product.has_variants? %>
+ <p class="prices">
+ <%= t("price") -%>
+ <br />
+ <span class="price selling"><%= product_price(@product) %></span><br />
+ </p>
+ <p class="volume_prices">
+ Volume prices available.
+ </p>
+<% elsif @product.master.progressive_volume_discount %>
+ <% current_price = @product.master.volume_prices.first %>
+ <% last_quantity = @product.master.volume_prices.first.starting_quantity %>
+ <p class="prices">
+ <%= t("price") -%>
+ <br />
+ <%= t 'first' %> <%= last_quantity - 1 %> <%= t 'at' %>
+ <span class="price selling"><%= product_price(@product) %></span> / ea.
+ <br />
+
+ <span style="font-size: 0.8em;">
+ <% @product.master.volume_prices[1..-1].each do |vp| -%>
+ <%= t 'next' %> <%= vp.starting_quantity - last_quantity %>
+ <%= t 'at' %>
+ <span class="price selling"><%= product_price(current_price) %></span>
+ / ea.<br />
+ <% current_price = vp %>
+ <% last_quantity = vp.starting_quantity %>
+ <% end %>
+ <%= t 'additional' %> <%= t 'at' %>
+ <span class="price selling"><%= product_price(current_price) %></span>
+ / ea.
+ </span>
+ </p>
+<% else %>
+ <p class="prices">
+ <%= t("price") -%>
+ <br />
+ <span class="price selling"><%= product_price(@product) %></span><br />
+ </p>
+ <p class="volume_prices">
+ Volume prices available:<br />
+ <span style="font-size: 0.8em;">
+ <% @product.master.volume_prices.each do |vp| -%>
+ <span class="price selling"><%= product_price(vp) %></span>
+ when buying <%= vp.starting_quantity %> or more<br />
+ <% end %>
+ </span>
+ </p>
<% end %>
View
11 config/locales/en.yml
@@ -1,6 +1,17 @@
en:
+ activerecord:
+ attributes:
+ product:
+ progressive_volume_discount: Progressive volume discount
+ variant:
+ progressive_volume_discount: Progressive volume discount
volume_pricing: Volume Pricing
volume_prices: Volume Prices
volume_discount: Volume Discount
starting_from: Starting from
add_volume_price: Add Volume Price
+ first: First
+ next: Next
+ additional: Additional
+ at: at
+ progressive_discount_info: By default, a single volume price selected based on the item's quantity is applied to all it's units. Select this option to have a progressively reduced price applied to successive units depending on how many are already in the cart, i.e., the first three at $10 each, the next three at $9 each, and all additional units at $8 apiece.
View
10 db/migrate/20110302223614_add_progressive_volume_discount_to_variants.rb
@@ -0,0 +1,10 @@
+class AddProgressiveVolumeDiscountToVariants < ActiveRecord::Migration
+ def self.up
+ add_column :variants, :progressive_volume_discount, :boolean,
+ :null => false, :default => 0
+ end
+
+ def self.down
+ remove_column :variants, :progressive_volume_discount
+ end
+end
View
2 spree_simple_volume_pricing.gemspec
@@ -3,7 +3,7 @@
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = 'spree_simple_volume_pricing'
- s.version = '2.0.0'
+ s.version = '2.1.0'
s.summary = 'Adds volume pricing capabilities to Spree'
s.author = 'Adam Wróbel'

0 comments on commit 991a786

Please sign in to comment.
Something went wrong with that request. Please try again.