# frozen_string_literal: true
# Second pass of the Atom custom-field import, for the companies the first pass missed.
#
# Maintenance::Atom::ImportCustomFieldsDedupedTask resolves a row's company with
# CompanyProfile.find_by(id: parent_id), which only works for the profiles the subcompany
# import created with the atom id as their primary key. A company that already existed
# in company-api keeps its own id and merely carries the atom company_guid, so its
# parent_id matched nothing and its custom fields were never attached — the gap this
# task closes.
#
# Each row is therefore resolved by company_guid instead, and a resolved profile whose
# id equals the row's parent_id is skipped: the first pass already attached that one,
# and re-attaching would be pointless work on every re-run.
#
# Grouping, CustomField upsert and validation-option/mapper reconciliation are shared
# with the first pass via Maintenance::Atom::CustomFieldGroupImportable, whose
# content-based lookup adopts the CustomField the first pass already created rather
# than duplicating it; only the missing MboProfile attachments are added.
#
# Expected CSV headers (the deduped export plus company_guid):
# name, validation_required, validation_regex,
# inclusion_list_validation_json, code_labels_json, auto_import_options,
# legacy_scope, legacy_agent_port, mapper_key,
# parent_id, company_guid, created_at, updated_at
#
# company_guid is the guid of the atom subcompany the row's parent_id points at. Rows
# with an unresolvable company_guid are logged and skipped rather than raising, so one
# unmigrated company does not abort the group; rows with a blank company_guid carry
# nothing to resolve and are dropped, also with a warning.
class Maintenance::Atom::ImportCustomFieldsByCompanyGuidTask < MaintenanceTasks::Task
include DatadogTrace
include Maintenance::Atom::CustomFieldGroupImportable
csv_collection
report_on(StandardError)
MAX_LOGGED_PARENT_IDS = 10
private
# A dedup group spans many rows, each with its own (parent_id, company_guid) pair, so
# the two are kept paired rather than collected into separate lists. Blank guids are
# dropped (nothing to resolve) and duplicate pairs collapsed.
def parent_attributes(group_rows)
{ 'parents' => collect_parents(group_rows) }
end
def collect_parents(group_rows)
resolvable, blank = group_rows.partition { |group_row| group_row['company_guid'].to_s.strip.present? }
log_blank_company_guids(group_rows.first['name'], blank) if blank.any?
resolvable.map { |group_row| parent_pair(group_row) }.uniq
end
def parent_pair(group_row)
{
'parent_id' => group_row['parent_id'].to_s.strip.presence,
'company_guid' => group_row['company_guid'].to_s.strip
}
end
# A blank company_guid loses that company its custom field for good — this task is the
# second and last pass — so the drop is reported rather than silent: it means the export
# shipped rows without a guid, which is a problem with the CSV, not with the data. Logged
# once per dedup group at collection time, with the parent_ids for tracing the rows back.
def log_blank_company_guids(name, blank_rows)
parent_ids = blank_rows.filter_map { |group_row| group_row['parent_id'].to_s.strip.presence }
Rails.logger.warn(
"CustomField '#{name}': #{blank_rows.size} row(s) with a blank company_guid, " \
"skipping mbo_profile attachment (parent_ids: #{format_parent_ids(parent_ids)})"
)
end
# Bounded: a group can span thousands of rows and the point of the line is the count
# plus a sample to grep the CSV with, not the full list.
def format_parent_ids(parent_ids)
return 'none' if parent_ids.empty?
listed = parent_ids.first(MAX_LOGGED_PARENT_IDS).join(', ')
remaining = parent_ids.size - MAX_LOGGED_PARENT_IDS
remaining.positive? ? "#{listed}, and #{remaining} more" : listed
end
def attach_parents(custom_field, group)
group['parents'].each { |parent| attach_by_company_guid(custom_field, parent) }
end
def attach_by_company_guid(custom_field, parent)
company_guid = parent['company_guid']
# find_by_company_guid normalizes case and legacy ATOM- guids, and returns nil for a
# value that cannot be a uuid instead of matching rows whose company_guid IS NULL.
company_profile = CompanyProfile.find_by_company_guid(company_guid)
return log_unresolved(custom_field, company_guid) unless company_profile
return if already_imported?(company_profile, parent['parent_id'])
attach_to_mbo_profiles(custom_field, company_profile)
end
# The profile's id IS the atom parent_id, so the id-based first pass already attached
# this field to it. Compared case-insensitively: the CSV carries the atom id verbatim
# while the uuid column stores it lowercased.
def already_imported?(company_profile, parent_id)
parent_id.present? && company_profile.id.casecmp?(parent_id.to_s.strip)
end
def log_unresolved(custom_field, company_guid)
Rails.logger.warn(
"CustomField #{custom_field.id}: no CompanyProfile with company_guid #{company_guid}, " \
'skipping mbo_profile attachment'
)
nil
end
end