BathHacked/energy-sparks

View on GitHub
app/models/energy_tariff.rb

Summary

Maintainability
A
1 hr
Test Coverage
# == Schema Information
#
# Table name: energy_tariffs
#
#  applies_to         :integer          default("both")
#  ccl                :boolean          default(FALSE)
#  created_at         :datetime         not null
#  created_by_id      :bigint(8)
#  enabled            :boolean          default(TRUE)
#  end_date           :date
#  id                 :bigint(8)        not null, primary key
#  meter_type         :integer          default("electricity"), not null
#  name               :text             not null
#  source             :integer          default("manually_entered"), not null
#  start_date         :date
#  tariff_holder_id   :bigint(8)
#  tariff_holder_type :string
#  tariff_type        :integer          default("flat_rate"), not null
#  tnuos              :boolean          default(FALSE)
#  updated_at         :datetime         not null
#  updated_by_id      :bigint(8)
#  vat_rate           :integer
#
# Indexes
#
#  index_energy_tariffs_on_created_by_id                            (created_by_id)
#  index_energy_tariffs_on_tariff_holder_type_and_tariff_holder_id  (tariff_holder_type,tariff_holder_id)
#  index_energy_tariffs_on_updated_by_id                            (updated_by_id)
#
# Foreign Keys
#
#  fk_rails_...  (created_by_id => users.id)
#  fk_rails_...  (updated_by_id => users.id)
#
class EnergyTariff < ApplicationRecord
  belongs_to :tariff_holder, polymorphic: true

  # Declaring associations allows us to use .joins(:school) or .joins(:school_group)
  belongs_to :school, -> { where(energy_tariffs: { tariff_holder_type: 'School' }) }, foreign_key: 'tariff_holder_id', optional: true
  belongs_to :school_group, -> { where(energy_tariffs: { tariff_holder_type: 'SchoolGroup' }) }, foreign_key: 'tariff_holder_id', optional: true

  delegated_type :tariff_holder, types: %w[SiteSettings School SchoolGroup]

  has_many :energy_tariff_prices, inverse_of: :energy_tariff, dependent: :destroy
  has_many :energy_tariff_charges, inverse_of: :energy_tariff, dependent: :destroy

  # only populated if tariff_holder is school
  has_and_belongs_to_many :meters, inverse_of: :energy_tariffs

  belongs_to :created_by, optional: true, class_name: 'User'
  belongs_to :updated_by, optional: true, class_name: 'User'

  enum source: [:manually_entered, :dcc]
  enum meter_type: [:electricity, :gas, :solar_pv, :exported_solar_pv]
  enum tariff_type: [:flat_rate, :differential]

  # Used as an all_energy_tariff_attributes filter:
  # :half_hourly applies to meters which have a :meter_system of :hh
  # :non_half_hourly applies to meters which have a :meter_system of :nhh_amr, :nhh or :smets2_smart
  # :both applies to meters with all meter :system_type values
  enum applies_to: [:both, :half_hourly, :non_half_hourly]

  validates :name, presence: true
  validates :vat_rate, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0, allow_nil: true }
  validates :applies_to, presence: true

  validate :start_and_end_date_are_not_both_blank
  validate :start_date_is_earlier_than_or_equal_to_end_date
  validate :applies_to_is_set_to_both, unless: :electricity?

  scope :enabled, -> { where(enabled: true) }
  scope :disabled, -> { where(enabled: false) }

  scope :by_name,       -> { order(name: :asc) }
  scope :by_start_date, -> { order(start_date: :asc) }

  # Sorts with null start date first, then start date, then end date
  scope :by_start_and_end, -> {
    order(Arel.sql('(CASE WHEN start_date is NULL THEN 0 ELSE 1 END) ASC, start_date asc, end_date asc'))
  }

  scope :count_by_school_group, -> { enabled.joins(:school_group).group(:slug).count(:id) }

  scope :for_schools_in_group, ->(school_group, source = :manually_entered) {
    enabled.where(source: source).joins(:school).where({ schools: { school_group: school_group } })
  }
  scope :count_schools_with_tariff_by_group, ->(school_group, source = :manually_entered) {
    for_schools_in_group(school_group, source).select(:tariff_holder_id).distinct.count
  }

  scope :latest_with_fixed_end_date, ->(meter_type, source = :manually_entered) { where(meter_type: meter_type, source: source).where.not(end_date: nil).order(end_date: :desc) }

  def applies_to_is_set_to_both
    return if electricity?
    return if both?

    errors.add(:applies_to, I18n.t('schools.user_tariffs.form.errors.applies_to.must_be_set_to_both'))
  end

  def self.usable
    select(&:usable?)
  end

  def usable?
    case tariff_type
    when 'differential' then usable_differential_tariff?
    when 'flat_rate' then usable_flat_rate_tariff?
    end
  end

  def flat_rate?
    tariff_type == 'flat_rate'
  end

  def meter_attribute
    MeterAttribute.new(attribute_type: :accounting_tariff_generic, input_data: to_hash)
  end

  def to_hash
    rates = rates_attrs
    {
      start_date: (start_date || Date.new(2000, 1, 1)).to_fs(:es_compact),
      end_date: (end_date || Date.new(2050, 1, 1)).to_fs(:es_compact),
      source: source.to_sym,
      name: name,
      type: flat_rate? ? :flat : :differential,
      sub_type: '',
      rates: rates,
      vat: vat_rate.nil? ? nil : "#{vat_rate}%",
      climate_change_levy: ccl,
      asc_limit_kw: (value_for_charge(:asc_limit_kw) if rates_has_availability_charge?(rates)),
      tariff_holder: tariff_holder_symbol,
      created_at: created_at.to_datetime
    }.compact
  end

  def value_for_charge(type)
    if (charge = energy_tariff_charges.for_type(type).first)
      charge.value.to_s
    end
  end

  def energy_tariff_refers_to_all_meters?
    tariff_holder.site_settings? || tariff_holder.school_group? || meters.empty?
  end

  # Used to the show page to decide whether there's content for the standing
  # charge section which groups these together
  def has_any_standing_charges?
    energy_tariff_charges.any? || tnuos? || vat_rate.present? || ccl?
  end

  private

  def usable_flat_rate_tariff?
    # For a flate rate energy tariff to be considered "usable":
    # * it must have only one energy tariff price record
    # * the price record must have a value set greater than zero
    return true if energy_tariff_prices.count == 1 && energy_tariff_prices&.first&.value&.nonzero?

    false
  end

  def usable_differential_tariff?
    # For a differential rate energy tariff to be considered "usable":
    # * it must have more two or more energy tariff price records
    # * the energy tariff price records combined start and end times must cover a full 24 hour period (1440 minutes)
    # * all energy tariff price records must have values set greater than zero
    return true if energy_tariff_prices.count >= 2 && energy_tariff_prices.complete? && energy_tariff_prices&.map(&:value)&.all? { |value| value&.nonzero? }

    false
  end

  def start_and_end_date_are_not_both_blank
    return unless tariff_holder_type == 'SchoolGroup'
    return if start_date.present? || end_date.present?

    errors.add(:start_date, I18n.t('schools.user_tariffs.form.errors.dates.start_and_end_date_can_not_both_be_empty'))
    errors.add(:end_date, I18n.t('schools.user_tariffs.form.errors.dates.start_and_end_date_can_not_both_be_empty'))
  end

  def start_date_is_earlier_than_or_equal_to_end_date
    return unless start_date.present? && end_date.present?
    return unless start_date > end_date

    errors.add(:start_date, I18n.t('schools.user_tariffs.form.errors.dates.start_date_must_be_earlier_than_or_equal_to_end_date'))
  end

  def tariff_holder_symbol
    meters.any? ? :meter : tariff_holder_type&.underscore&.to_sym
  end

  def rates_attrs
    attrs = {}
    if flat_rate?
      if (first_price = energy_tariff_prices.first)
        attrs[:flat_rate] = { rate: first_price.value.to_s, per: first_price.units.to_s }
      end
    else
      energy_tariff_prices.each_with_index do |price, idx|
        attrs["rate#{idx}".to_sym] = { rate: price.value.to_s, per: price.units.to_s, from: hour_minutes(price.start_time), to: hour_minutes(price.end_time.advance(minutes: -30)) }
      end
    end
    energy_tariff_charges.select { |c| c.units.present? }.each do |charge|
      charge_value = { rate: charge.value.to_s, per: charge.units.to_s }
      charge_type = charge.charge_type.to_sym
      # only add these charges if we also have an asc limit
      if charge.is_type?([:agreed_availability_charge, :excess_availability_charge])
        attrs[charge_type] = charge_value if value_for_charge(:asc_limit_kw).present?
      else
        attrs[charge_type] = charge_value
      end
    end
    energy_tariff_charges.select { |c| c.is_type?([:duos_red, :duos_amber, :duos_green]) }.each do |charge|
      attrs[charge.charge_type.to_sym] = charge.value.to_s
    end
    attrs[:tnuos] = tnuos
    attrs
  end

  def rates_has_availability_charge?(rates)
    rates.key?(:agreed_availability_charge) || rates.key?(:excess_availability_charge)
  end

  def hour_minutes(time)
    hm = time.to_fs(:time).split(':')
    {
      hour: hm.first,
      minutes: hm.last
    }
  end
end