Maintenance::MergeOrphanTreenodesTask

Source code
# frozen_string_literal: true

class Maintenance::MergeOrphanTreenodesTask < MaintenanceTasks::Task
  include ArrayHelper

  csv_collection

  attribute :emails, :string
  validates :emails, presence: true, fcm_email_format: true

  REPORT_COLUMNS = %w[
    orphan_treenode_id
    mbo_id
    mbo_profile_id
    mid_back_office
    code
    target_treenode_id
    status
    errors
  ].freeze

  HEADERS = {
    orphan_treenode_id: 'orphan treenode id (without company guid)',
    mbo_id: 'mbo_id',
    mbo_profile_id: 'mbo_profile_id',
    mid_back_office: 'mid_back_office',
    country_code: 'code',
    target_treenode_id: 'treenode to merge to (with Company GUID)'
  }.freeze

  after_start :prepare_csv_path
  after_complete :send_report
  after_error :send_report

  def process(row)
    result = process_row(row)
    status = result.blank? ? 'SUCCESS' : 'FAILED'
    append_report(row, status: status, errors: result)
  rescue StandardError => e
    append_report(row, status: 'FAILED', errors: e.message)
    raise
  end

  def csv_path
    @csv_path ||= Rails.root.join('tmp', "merge_orphan_treenodes_#{Time.now.to_i}.csv").to_s
  end

  private

  def process_row(row)
    source_tree_node, target_tree_node = fetch_tree_nodes(row)
    if invalid_tree_nodes?(source_tree_node, target_tree_node)
      return 'Source or target tree node not found, or same tree node'
    end

    source_mbo_profile = find_source_mbo_profile(row, source_tree_node)
    return 'Source mbo_profile not found in orphan tree node' unless source_mbo_profile

    move_profile_to_target(
      source_tree_node: source_tree_node,
      target_tree_node: target_tree_node,
      mbo_profile: source_mbo_profile
    )
    ''
  end

  def prepare_csv_path
    @csv_path = Rails.root.join('tmp', "merge_orphan_treenodes_#{Time.now.to_i}.csv").to_s
    File.write(csv_path, REPORT_COLUMNS.to_csv)
  end

  def send_report
    return unless csv_path && File.exist?(csv_path)

    CsvReportMailer.send_report(
      recipients: emails_array(emails),
      file_path: csv_path,
      report_sender: 'Merge Orphan Treenodes'
    ).deliver_now
  ensure
    File.delete(csv_path) if csv_path && File.exist?(csv_path)
  end

  def append_report(row, status:, errors:)
    File.open(csv_path, 'a') do |file|
      file.puts(
        [
          row_value(row, :orphan_treenode_id),
          row_value(row, :mbo_id),
          row_value(row, :mbo_profile_id),
          row_value(row, :mid_back_office),
          row_value(row, :country_code),
          row_value(row, :target_treenode_id),
          status,
          errors
        ].to_csv
      )
    end
  end

  def fetch_tree_nodes(row)
    source_tree_node = TreeNode.find_by(id: row_value(row, :orphan_treenode_id))
    target_tree_node = TreeNode.find_by(id: row_value(row, :target_treenode_id))
    [source_tree_node, target_tree_node]
  end

  def invalid_tree_nodes?(source_tree_node, target_tree_node)
    source_tree_node.blank? || target_tree_node.blank? || source_tree_node.id == target_tree_node.id
  end

  def row_value(row, key)
    row.field(HEADERS.fetch(key)).to_s.strip
  end

  def find_source_mbo_profile(row, source_tree_node)
    source_tree_node.mbo_profiles.find_by(id: row_value(row, :mbo_profile_id))
  end

  # :reek:FeatureEnvy
  def move_profile_to_target(source_tree_node:, target_tree_node:, mbo_profile:)
    join = TreeNodeToMboProfile.find_by(tree_node_id: source_tree_node.id, mbo_profile_id: mbo_profile.id)
    return unless join

    existing_target_join = TreeNodeToMboProfile.find_by(
      tree_node_id: target_tree_node.id,
      mbo_profile_id: mbo_profile.id
    )

    if existing_target_join
      join.destroy!
    else
      join.update!(tree_node_id: target_tree_node.id, primary: false)
    end

    ensure_single_primary_mbo_profile(target_tree_node)
  end

  def ensure_single_primary_mbo_profile(tree_node)
    primary_joins = tree_node.tree_node_to_mbo_profiles.reload.where(primary: true).order(:created_at)
    all_joins = tree_node.tree_node_to_mbo_profiles.order(:created_at)

    if primary_joins.empty? && all_joins.any?
      all_joins.first.update!(primary: true)
    elsif primary_joins.many?
      primary_joins.offset(1).find_each { |item| item.update!(primary: false) }
    end
  end
end