ManageIQ/manageiq

View on GitHub
app/models/chargeback_rate_detail.rb

Summary

Maintainability
A
3 hrs
Test Coverage
B
85%
class ChargebackRateDetail < ApplicationRecord
  belongs_to :chargeback_rate
  belongs_to :chargeable_field
  belongs_to :detail_measure, :class_name => "ChargebackRateDetailMeasure", :foreign_key => :chargeback_rate_detail_measure_id
  belongs_to :detail_currency, :class_name => "Currency", :foreign_key => :chargeback_rate_detail_currency_id, :inverse_of => :chargeback_rate_detail
  has_many :chargeback_tiers, :dependent => :destroy, :autosave => true

  default_scope { joins(:chargeable_field).merge(ChargeableField.order(:group => :asc, :description => :asc)) }

  validates :chargeback_rate, :chargeable_field, :presence => true
  validate :contiguous_tiers?

  delegate :rate_type, :to => :chargeback_rate, :allow_nil => true

  delegate :metric_column_key, :metric_key, :cost_keys, :rate_key, :to => :chargeable_field

  FORM_ATTRIBUTES = %i[description per_time per_unit metric group source metric chargeable_field_id sub_metric].freeze
  PER_TIME_TYPES = {
    "hourly"  => N_("Hourly"),
    "daily"   => N_("Daily"),
    "weekly"  => N_("Weekly"),
    "monthly" => N_("Monthly"),
    'yearly'  => N_('Yearly')
  }.freeze

  # gigabytes -> GiB
  #
  def showback_unit(p_per_unit = nil)
    return '' unless chargeable_field.detail_measure

    {'bytes'     => '',
     'kilobytes' => 'KiB',
     'megabytes' => 'MiB',
     'gigabytes' => 'GiB',
     'terabytes' => 'TiB',
     'hertz'     => '',
     'kilohertz' => 'KHz',
     'megahertz' => 'MHz',
     'gigahertz' => 'GHz',
     'teraherts' => 'THz',
     'bps'       => '',
     'kbps'      => 'Mbps',
     'mbps'      => 'Gbps',
     'gbps'      => 'Tbps'}[p_per_unit || per_unit]
  end

  def populate_showback_rate(plan, rate_detail, entity)
    group = rate_detail.chargeable_field.showback_measure
    field, _, calculation = rate_detail.chargeable_field.showback_dimension
    unit  = rate_detail.showback_unit

    showback_rate = ManageIQ::Showback::Rate.find_or_create_by(:entity      => entity,
                                                               :group       => group,
                                                               :field       => field,
                                                               :price_plan  => plan,
                                                               :calculation => calculation,
                                                               :concept     => rate_detail.id)
    showback_rate.tiers.destroy_all
    rate_detail.chargeback_tiers.each do |tier|
      showback_rate.tiers.build(:tier_start_value       => tier.start,
                                :tier_end_value         => tier.finish,
                                :variable_rate_per_time => rate_detail.per_time,
                                :variable_rate_per_unit => unit,
                                :fixed_rate_per_time    => rate_detail.per_time,
                                :fixed_rate             => Money.new(tier.fixed_rate * Money.default_currency.subunit_to_unit),
                                :variable_rate          => Money.new(tier.variable_rate * Money.default_currency.subunit_to_unit))
    end
    showback_rate.save
  end

  def sub_metrics
    if metric == 'derived_vm_allocated_disk_storage'
      volume_types = CloudVolume.volume_types
      unless volume_types.empty?
        res = {}
        res[_('All')] = ''
        volume_types.each { |type| res[type.capitalize] = type }
        res[_('Other - Unclassified')] = 'unclassified'
        res
      end
    end
  end

  def sub_metric_human
    sub_metric.present? ? sub_metric.capitalize : 'All'
  end

  def format_rate(rate, suffix = "")
    MiqReport.new.format_currency_with_delimiter(rate, :unit => detail_currency.symbol) + " / " + PER_TIME_TYPES[per_time] + suffix
  end

  def format_unit(rate_unit)
    unit = showback_unit(rate_unit)
    unit.presence || Dictionary.gettext(per_unit, :notfound => :titleize)
  end

  def rate_values(consumption, options)
    fixed_rate, variable_rate = find_rate(chargeable_field.measure(consumption, options, sub_metric))

    rates = []
    rates.push(format_rate(fixed_rate)) if fixed_rate > 0.0
    rates.push(format_rate(variable_rate, " / #{format_unit(per_unit)}")) if variable_rate > 0.0

    rates.join(" + ")
  end

  def charge(consumption, options)
    result = {}

    metric_value, cost = metric_and_cost_by(consumption, options)
    if !consumption.chargeback_fields_present && chargeable_field.fixed?
      cost = 0
    end

    result[rate_key(sub_metric)] = rate_values(consumption, options)
    result[metric_key(sub_metric)] = metric_value
    cost_keys(sub_metric).each { |field| result[field] = cost }

    result
  end

  # Set the rates according to the tiers
  def find_rate(value)
    @found_rates ||= {}
    @found_rates[value] ||=
      begin
        fixed_rate = 0.0
        variable_rate = 0.0
        tier_found = chargeback_tiers.detect { |tier| tier.includes?(value * rate_adjustment) }
        unless tier_found.nil?
          fixed_rate = tier_found.fixed_rate
          variable_rate = tier_found.variable_rate
        end

        [fixed_rate, variable_rate]
      end
  end

  PER_TIME_MAP = {
    :hourly  => "Hour",
    :daily   => "Day",
    :weekly  => "Week",
    :monthly => "Month",
    :yearly  => "Year"
  }

  def hourly_cost(value, consumption)
    return 0.0 unless enabled?

    (fixed_rate, variable_rate) = find_rate(value)

    hourly_fixed_rate    = hourly(fixed_rate, consumption)
    hourly_variable_rate = hourly(variable_rate, consumption)

    hourly_fixed_rate + rate_adjustment * value * hourly_variable_rate
  end

  def hourly(rate, consumption)
    case per_time
    when "hourly"  then rate
    when "daily"   then rate / 24
    when "weekly"  then rate / 24 / 7
    when "monthly" then rate / consumption.hours_in_month
    when "yearly"  then rate / 24 / 365
    else raise "rate time unit of '#{per_time}' not supported"
    end
  end

  def rate_adjustment
    @rate_adjustment ||= chargeable_field.adjustment_to(per_unit)
  end

  def affects_report_fields(report_cols)
    ([metric_key].to_set & report_cols).present? || ((cost_keys.to_set & report_cols).present? && !gratis?)
  end

  def friendly_rate
    (fixed_rate, variable_rate) = find_rate(0.0)
    value = read_attribute(:friendly_rate)
    return value unless value.nil?

    if chargeable_field.fixed?
      # Example: 10.00 Monthly
      "#{fixed_rate + variable_rate} #{per_time.to_s.capitalize}"
    else
      s = ""
      chargeback_tiers.each do |tier|
        # Example: Daily @ .02 per MHz from 0.0 to Infinity
        s += "#{per_time.to_s.capitalize} @ #{tier.fixed_rate} + " \
             "#{tier.variable_rate} per #{per_unit_display} from #{tier.start} to #{tier.finish}\n"
      end
      s.chomp
    end
  end

  def per_unit_display
    measure = chargeable_field.detail_measure
    measure.nil? ? per_unit.to_s.capitalize : measure.measures.key(per_unit)
  end

  # New method created in order to show the rates in a easier to understand way
  def show_rates
    hr = ChargebackRateDetail::PER_TIME_MAP[per_time.to_sym]
    rate_display = "#{detail_currency.symbol} [#{detail_currency.full_name}] / #{hr}"
    rate_display_unit = "#{rate_display} / #{per_unit_display}"
    per_unit.nil? ? rate_display : rate_display_unit
  end

  def save_tiers(tiers)
    temp = self.class.new(:chargeback_tiers => tiers)
    if temp.contiguous_tiers?
      chargeback_tiers.replace(tiers)
    else
      temp.errors.each { |error| errors.add(error.attribute, error.message) }
    end
  end

  # Check that tiers are complete and disjoint
  def contiguous_tiers?
    error = false

    # Note, we use sort_by vs. order since we need to call this method against
    # the in memory chargeback_tiers association and NOT hit the database.
    tiers = chargeback_tiers

    tiers.each_with_index do |tier, index|
      if single_tier?(tier, tiers)
        error = true if !tier.starts_with_zero? || !tier.ends_with_infinity?
      elsif first_tier?(tier, tiers)
        error = true if !tier.starts_with_zero? || tier.ends_with_infinity?
      elsif last_tier?(tier, tiers)
        error = true if !consecutive_tiers?(tier, tiers[index - 1]) || !tier.ends_with_infinity?
      elsif middle_tier?(tier, tiers)
        error = true if !consecutive_tiers?(tier, tiers[index - 1]) || tier.ends_with_infinity?
      end

      break if error
    end

    errors.add(:chargeback_tiers, _("must start at zero and not contain any gaps between start and prior end value.")) if error

    !error
  end

  private

  def gratis?
    chargeback_tiers.all?(&:gratis?)
  end

  def metric_and_cost_by(consumption, options)
    metric_value = chargeable_field.measure(consumption, options, sub_metric)
    hourly_cost = hourly_cost(metric_value, consumption)

    _log.debug("Consumption interval: #{consumption.consumption_start} -  #{consumption.consumption_end}")
    _log.debug("Consumed hours: #{consumption.consumed_hours_in_interval}")
    cost = chargeable_field.metering? ? hourly_cost : hourly_cost * consumption.consumed_hours_in_interval
    [metric_value, cost]
  end

  def first_tier?(tier, tiers)
    tier == tiers.first
  end

  def last_tier?(tier, tiers)
    tier == tiers.last
  end

  def single_tier?(tier, tiers)
    first_tier?(tier, tiers) && last_tier?(tier, tiers)
  end

  def middle_tier?(tier, tiers)
    !first_tier?(tier, tiers) && !last_tier?(tier, tiers)
  end

  def consecutive_tiers?(tier, previous_tier)
    tier.start == previous_tier.finish
  end

  def self.default_rate_details_for(rate_type)
    rate_details = []

    fixture_file = File.join(FIXTURE_DIR, "chargeback_rates.yml")
    fixture = File.exist?(fixture_file) ? YAML.load_file(fixture_file) : []
    fixture.each do |chargeback_rate|
      next unless chargeback_rate[:rate_type] == rate_type && chargeback_rate[:description] == "Default"

      chargeback_rate[:rates].each do |detail|
        detail_new = ChargebackRateDetail.new(detail.slice(*ChargebackRateDetail::FORM_ATTRIBUTES))
        detail_new.detail_currency = Currency.find_by(:code => detail[:type_currency])
        detail_new.metric = detail[:metric]
        detail_new.chargeable_field = ChargeableField.find_by(:metric => detail.delete(:metric))

        detail[:tiers].sort_by { |tier| tier[:start] }.each do |tier|
          detail_new.chargeback_tiers << ChargebackTier.new(tier.slice(*ChargebackTier::FORM_ATTRIBUTES))
        end

        rate_details.push(detail_new)

        if detail_new.chargeable_field.metric == 'derived_vm_allocated_disk_storage'
          volume_types = CloudVolume.volume_types
          volume_types.push('unclassified') if volume_types.present?
          volume_types.each do |volume_type|
            storage_detail_new = detail_new.dup
            storage_detail_new.sub_metric = volume_type
            detail[:tiers].sort_by { |tier| tier[:start] }.each do |tier|
              storage_detail_new.chargeback_tiers << ChargebackTier.new(tier.slice(*ChargebackTier::FORM_ATTRIBUTES))
            end
            rate_details.push(storage_detail_new)
          end
        end
      end
    end

    rate_details.sort_by { |rd| [rd.chargeable_field[:group], rd.chargeable_field[:description], rd[:sub_metric].to_s] }
  end
end