Created
January 9, 2013 16:00
-
-
Save iloveitaly/4494282 to your computer and use it in GitHub Desktop.
N for X promotion calculator for spree commerce. (example: "5 for $50")
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Spree | |
class Calculator::NForX < Calculator | |
preference :number, :integer, :default => 0 # n | |
preference :amount, :decimal, :default => 0 # x | |
attr_accessible :preferred_amount | |
attr_accessible :preferred_number | |
def self.description | |
I18n.t(:n_for_x) | |
end | |
# WARNING this calculator should not be used with promotions whose applicable line items' price can differ from | |
# the variants price field. In this case the line items price will not be used, the line item's variants price | |
# will be used instead | |
def compute(object=nil) | |
return 0 if object.nil? || group_count(object) == 0 | |
discount = applicable_variants(object).map(&:price).sum - (self.preferred_amount * group_count) | |
return 0 if discount < 0 | |
discount | |
end | |
def report(object = nil) | |
# unfortunately there is no way to attribute a discount to an individual item in a line item if multiple | |
# items of the same variant were purchased. If someone purchased 4 of one product and five of another in | |
# a 5 for $50 deal then the discount for the one product of the five purchased would have to be spread | |
# over the 5 products even though the discount only applied to one. | |
# This isn't completely accurate, but it is the best we can do. | |
# line_items.dup does not duplicate the individual line item objects, we have to handle them individually | |
discount = compute(object) | |
applicable_total = applicable_variants.map(&:price).sum | |
discount_schedule = {} | |
applicable_variants.map(&:id).uniq.each do |variant_id| | |
percentage = applicable_variants.select { |v| v.id == variant_id }.map(&:price).sum / applicable_total | |
discount_schedule[variant_id] = percentage | |
end | |
object.line_items.map(&:dup).each do |line_item| | |
next unless applicable_variants.include? line_item.variant | |
# TODO some minor rounding errors might be happening here | |
# possibly use round_to_two_places() (not sure where that method is defined) | |
line_item.price -= discount_schedule[line_item.variant.id] * discount / line_item.quantity | |
end | |
end | |
private | |
def applicable_variants(object = nil) | |
return @applicable_variants if !@applicable_variants.nil? | |
@applicable_variants = [] | |
matching_line_items(object).each do |l| | |
# not DRY, but there is not an easy way to break nested ruby loops | |
break if @applicable_variants.size == group_count(object) * self.preferred_number | |
l.quantity.times do | |
@applicable_variants << l.variant | |
break if @applicable_variants.size == group_count * self.preferred_number | |
end | |
end | |
@applicable_variants | |
end | |
def group_count(object = nil) | |
@group_count ||= matching_line_items(object).map(&:quantity).sum / self.preferred_number | |
end | |
def matching_line_items(object = nil) | |
# TODO if a bunch of items are the same price the sort order might be undefined | |
# look into this and ensure that there are consistent results | |
@matching_line_items ||= object.line_items.select { |l| matching_variants.include? l.variant }.sort_by! { |l| l.variant.price } | |
end | |
# Returns all variants that match the promotion's rule. | |
def matching_variants | |
@matching_variants ||= if compute_on_promotion? | |
self.calculable.promotion.rules.select { |r| r.respond_to? :variants }.map(&:variants).flatten | |
end | |
end | |
# Determines wether or not the calculable object is a promotion | |
def compute_on_promotion? | |
return true | |
@compute_on_promotion ||= self.calculable.respond_to?(:promotion) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment