app/models/package.rb

Summary

Maintainability
A
3 hrs
Test Coverage
B
83%
# frozen_string_literal: true

# == Schema Information
#
# Table name: packages
#
#  id                         :bigint(8)        not null, primary key
#  data_element_group_ext_ref :string           not null
#  deg_external_reference     :string
#  description                :string
#  frequency                  :string           not null
#  groupsets_ext_refs         :string           default([]), is an Array
#  include_main_orgunit       :boolean          default(FALSE), not null
#  kind                       :string           default("single")
#  name                       :string           not null
#  ogs_reference              :string
#  created_at                 :datetime         not null
#  updated_at                 :datetime         not null
#  loop_over_combo_ext_id     :string
#  project_id                 :integer
#  stable_id                  :uuid             not null
#
# Indexes
#
#  index_packages_on_project_id  (project_id)
#
# Foreign Keys
#
#  fk_rails_...  (project_id => projects.id)
#

class Package < ApplicationRecord
  include PaperTrailed
  delegate :program_id, to: :project

  FREQUENCIES = %w[monthly quarterly yearly].freeze
  KINDS = %w[single multi-groupset zone].freeze
  belongs_to :project, inverse_of: :packages
  has_many :package_entity_groups, dependent: :destroy
  has_many :package_states, dependent: :destroy
  has_many :states, through: :package_states, source: :state
  has_many :rules, dependent: :destroy
  has_many :activity_packages, dependent: :destroy

  has_many :activities, -> { order(code: :asc) }, through: :activity_packages, source: :activity

  has_many :package_payment_rules
  has_many :payment_rules, through: :package_payment_rules, source: :payment_rule

  validates :name, presence: true, length: { maximum: 50 }

  validates :frequency, presence: true, inclusion: {
    in:      FREQUENCIES,
    message: "%{value} is not a valid see #{FREQUENCIES.join(',')}"
  }

  validates :kind, presence: true, inclusion: {
    in:      KINDS,
    message: "%{value} is not a valid see #{KINDS.join(',')}"
  }

  accepts_nested_attributes_for :states

  def code
    @code ||= Orbf::RulesEngine::Codifier.codify(name)
  end

  def invoice_details
    states.map(&:code) + activity_rule.formulas.map(&:code) + ["activity_name"]
  end

  def package_state(state)
    package_states.find { |ps| ps.state_id == state.id }
  end

  def activity_states(state)
    activities.flat_map(&:activity_states).select { |activity_state| activity_state.state == state }
  end

  def monthly?
    frequency == "monthly"
  end

  def quarterly?
    frequency == "quarterly"
  end

  def yearly?
    frequency == "yearly"
  end

  def multi_entities?
    kind == "multi-groupset"
  end

  def zone_kind?
    kind == "zone"
  end

  def missing_rules_kind
    %w[activity package multi-entities zone zone_activity] - rules.map(&:kind)
  end

  def allowed_rules
    allowed_rules = [
      Rule::RULE_TYPE_PAYMENT,
      Rule::RULE_TYPE_ACTIVITY,
      Rule::RULE_TYPE_PACKAGE,
      Rule::RULE_TYPE_MULTI_ENTITIES
    ]
    if zone_kind?
      allowed_rules << Rule::RULE_TYPE_ZONE
      allowed_rules << Rule::RULE_TYPE_ZONE_ACTIVITY
    end
    allowed_rules
  end

  def rule_allowed?(rule_kind:)
    allowed_rules.include?(rule_kind)
  end

  def already_has_rule?(rule_kind:)
    rules.where(kind: rule_kind).any?
  end

  def configured?
    activity_rule && package_rule
  end

  def for_frequency(frequency_to_apply)
    frequency_to_apply == frequency
  end

  def multi_entities_rule
    rules.find(&:multi_entities_kind?)
  end

  def package_rule
    rules.find(&:package_kind?)
  end

  def activity_rule
    rules.find(&:activity_kind?)
  end

  def zone_activity_rule
    rules.find(&:zone_activity_kind?)
  end

  def zone_rule
    rules.find(&:zone_kind?)
  end

  def kind_multi?
    kind.start_with?("multi-")
  end

  def main_entity_groups
    package_entity_groups.select(&:main?)
  end

  def target_entity_groups
    package_entity_groups.select(&:target?)
  end

  # A org_unit that can be used in the simulation, to get data for
  # this package. This is a expensive call, that will probably be not
  # needed anymore when the simulation ouput gets splitted into many
  # parts.
  #
  # TODO: Refactor away the expensiveness
  def simulation_org_unit
    org_unit_group = main_entity_groups.first&.organisation_unit_group_ext_ref
    return nil unless org_unit_group

    Pyramid.from(project).org_units_in_group(org_unit_group).first
  end

  def org_unit_group_sets
    completer = Autocomplete::Dhis2.new(project.project_anchor)
    (groupsets_ext_refs || []).each_with_object([]) do |ref, result|
      data = { id: ref }
      search_results = completer.find(ref, kind: "organisation_unit_group_sets")
      item = search_results.first
      if item
        data.merge!(type:         "organisation_unit_group_set",
                    display_name: item.display_name)
      end
      result << data
    end
  end

  def missing_activity_states
    missing_activity_states = {}
    activities.each do |activity|
      missing_states = states.map do |state|
        state unless activity.activity_state(state)
      end
      missing_activity_states[activity] = missing_states.reject(&:nil?)
    end
    missing_activity_states
  end

  def create_data_element_group(data_element_ids)
    deg = [
      { name:          name,
        short_name:    name[0..49],
        code:          name[0..49],
        display_name:  name,
        data_elements: data_element_ids.map do |data_element_id|
          { id: data_element_id }
        end }
    ]
    dhis2 = project.dhis2_connection
    status = dhis2.data_element_groups.create(deg)
    created_deg = dhis2.data_element_groups.find_by(name: name)
    raise "data element group not created #{name} : #{deg} : #{status.inspect}" unless created_deg

    created_deg
  rescue RestClient::Exception => e
    raise "Failed to create data element group #{deg} #{e.message} with #{project.dhis2_url}"
  end

  def create_package_entity_groups(main_entity_groups_ids, target_entity_groups_ids)
    dhis2 = project.dhis2_connection
    all_group_ids = (main_entity_groups_ids || []) + (target_entity_groups_ids || [])
    organisation_unit_groups = dhis2.organisation_unit_groups.find(all_group_ids)

    organisation_unit_groups.map do |organisation_unit_group|
      belong_to_main = main_entity_groups_ids.include?(organisation_unit_group.id)
      {
        name:                            organisation_unit_group.display_name,
        kind:                            belong_to_main ? "main" : "target",
        organisation_unit_group_ext_ref: organisation_unit_group.id
      }
    end
  end

  def to_unified_h
    {
      stable_id:             stable_id,
      name:                  name,
      states:                states.map do |state|
        { code: state.code }
      end,
      activity_packages:     Hash[
        activity_packages.flat_map(&:activity).map(&:to_unified_h).map do |activity|
          [activity[:stable_id], activity]
        end
      ],
      package_entity_groups: package_entity_groups.map do |entity_group|
        {
          external_reference: entity_group.organisation_unit_group_ext_ref,
          name:               name
        }
      end,

      rules:                 Hash[
        rules.map(&:to_unified_h).map do |rule|
          [rule[:stable_id], rule]
        end
      ]
    }
  end

  def to_s
    "Package-#{id}-#{name}"
  end
end