houdini/lib/calculate/calculate_suggested_amounts.rb

84 lines
2.6 KiB
Ruby

# frozen_string_literal: true
# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
require 'numeric'
module CalculateSuggestedAmounts
MIN = 25
MAX = 100_000_000
BRACKETS = [{ range: MIN...1000, delta: 100 },
{ range: 1000...5000, delta: 500 },
{ range: 5000...MAX, delta: 2500 }].freeze
# Calculates a set of suggested donation amounts based upon our internal special algorithm
# This is most useful for suggesting amounts a recurring donor could change to
# @return [Array<Integer>] suggested amounts for your donation
# @param [Number] amount the amount in cents to start from
def self.calculate(amount)
ParamValidation.new({ amount: amount }, amount: { required: true, is_a: Numeric, min: MIN, max: MAX })
result = []
step_down_val = step_down_value(amount)
result.push(step_down_val) unless step_down_val.nil?
higher_amounts = []
while higher_amounts.empty? || (higher_amounts.length < 3 && !higher_amounts.last.nil?)
if higher_amounts.empty?
higher_amounts.push(step_up_value(amount))
else
higher_amounts.push(step_up_value(higher_amounts.last))
end
end
result.concat(higher_amounts.reject(&:nil?))
end
def self.step_down_value(amount)
initial_bracket = get_bracket_by_amount(amount)
# check_floor_for_delta
delta_floor = amount.floor_for_delta(initial_bracket[:delta])
# not on a delta, just send a floor
if delta_floor != amount
return delta_floor < MIN ? nil : delta_floor
end
potential_lower_amount = amount - initial_bracket[:delta]
# is potential_lower_amount < our MIN? if so, return nil
return nil if potential_lower_amount < MIN
new_bracket = get_bracket_by_amount(potential_lower_amount)
# if in same bracket, potential_lower_amount is our step_down_value
return potential_lower_amount if initial_bracket == new_bracket
# we're going to step down by our new bracket value then
amount - new_bracket[:delta]
end
def self.step_up_value(amount)
bracket = get_bracket_by_amount(amount)
# check_ceil_for_delta
delta_ceil = amount.ceil_for_delta(bracket[:delta])
# not on a delta, just send a ceil
if delta_ceil != amount
return delta_ceil >= MAX ? nil : delta_ceil
end
potential_higher_amount = amount + bracket[:delta]
# is potential_lower_amount < our MIN? if so, return nil
return nil if potential_higher_amount >= MAX
potential_higher_amount
end
def self.get_bracket_by_amount(amount)
BRACKETS.detect { |i| i[:range].cover?(amount) }
end
end