Skip to content

Instantly share code, notes, and snippets.

@iloveitaly
Created January 9, 2013 16:00
Show Gist options
  • Save iloveitaly/4494282 to your computer and use it in GitHub Desktop.
Save iloveitaly/4494282 to your computer and use it in GitHub Desktop.
N for X promotion calculator for spree commerce. (example: "5 for $50")
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