Maintenance::ArchiveTreeNodesByCompanyGuidTask

Source code
# frozen_string_literal: true

# Archives TreeNodes whose Company GUID appears in the uploaded CSV.
#
# Reconciles profiles that were deleted in AgentPort during a sync-outage
# window but never archived on our side (ADO #71255). Only the `status`
# column is changed; rows with a blank Company GUID, GUIDs with no matching
# TreeNode, and already-archived nodes are skipped.
#
# Every row's outcome (archived / already_archived / not_found /
# blank_company_guid) is written to a CSV report, which is emailed to the
# operator-supplied recipients when the run completes or errors — this is the
# per-Company-GUID confirmation required by acceptance criteria #4.
#
# The CSV is expected to expose a `CompanyGUID` column (the header used in the
# customer-profile backfill report). Because `tree_nodes.company_guid` is
# unique, each GUID matches at most one TreeNode.
class Maintenance::ArchiveTreeNodesByCompanyGuidTask < MaintenanceTasks::Task
  include ArrayHelper

  csv_collection

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

  HEADER_COMPANY_GUID = 'CompanyGUID'
  REPORT_COLUMNS = %w[company_guid result tree_node_id].freeze
  REPORT_SENDER = 'Archive Tree Nodes By Company GUID'

  after_start :prepare_csv_path
  after_complete :send_report
  after_error :send_report

  def process(row)
    company_guid = row.field(HEADER_COMPANY_GUID)&.strip
    # A blank GUID would match a TreeNode with NULL/empty company_guid (both
    # allowed by the schema) and archive an unrelated node, so skip it.
    return append_report(company_guid, :blank_company_guid) if company_guid.blank?

    tree_node = TreeNode.find_by_company_guid(company_guid)
    return append_report(company_guid, :not_found) if tree_node.nil?

    if tree_node.status == TreeNodes::Statuses::ARCHIVED
      return append_report(company_guid, :already_archived, tree_node)
    end

    tree_node.update!(status: TreeNodes::Statuses::ARCHIVED)
    append_report(company_guid, :archived, tree_node)
  end

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

  private

  def prepare_csv_path
    @csv_path = Rails.root.join('tmp', "archive_tree_nodes_by_company_guid_#{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: REPORT_SENDER
    ).deliver_now
  ensure
    File.delete(csv_path) if csv_path && File.exist?(csv_path)
  end

  def append_report(company_guid, result, tree_node = nil)
    File.open(csv_path, 'a') do |file|
      file.puts([company_guid, result, tree_node&.id].to_csv)
    end
  end
end