Maintenance::BackfillMboProfileAgentPortSourceTask

Source code
# frozen_string_literal: true

##
# Stamps +agent_port_source+ on AgentPort profiles created before provenance existed.
#
# Deliberately conservative. It is an optimisation, not a prerequisite: profiles left
# NULL are adopted and stamped by MboProfileFallback on their next sync. Guessing wrong
# here is far worse than guessing nothing, because a wrong slot makes two profiles of the
# same company collide on one key and the reconciler then prunes the loser.
#
# Only LUMINA and PROTAS are inferable. D365 is not: RF:UDID:53 and CRI:U53 both land on
# it. AIAN and CLIENT_ACCOUNT_NUMBER are not: both fall back to the country default MBO
# and are indistinguishable after the fact.
class Maintenance::BackfillMboProfileAgentPortSourceTask < MaintenanceTasks::Task
  UNAMBIGUOUS_SLOTS = {
    MboProfiles::MidBackOffice::LUMINA => 'RF:UDID:44',
    MboProfiles::MidBackOffice::PROTAS => 'RF:UDID:52'
  }.freeze

  collection_batch_size(1000)

  def collection
    MboProfile.where(
      sync_data_source: MboProfiles::SyncDataSource::AGENT_PORT,
      agent_port_source: nil,
      mid_back_office: UNAMBIGUOUS_SLOTS.keys
    )
  end

  def process(element)
    slot = UNAMBIGUOUS_SLOTS[element.mid_back_office]
    return if slot.blank?
    return if country_default_mbo?(element)
    return if sibling_shares_mid_back_office?(element)

    # rubocop:disable Rails/SkipsModelValidations
    element.update_column(:agent_port_source, slot)
    # rubocop:enable Rails/SkipsModelValidations
  rescue StandardError => e
    Rails.logger.error("Failed to backfill agent_port_source for MboProfile #{element.id}: #{e.message}")
  end

  private

  # The country default is what AIAN, ClientAccountNumber and every unmapped reporting
  # slot fall back to, so it proves nothing about provenance.
  def country_default_mbo?(element)
    element.country&.default_mbo_to_country&.default_mbo == element.mid_back_office
  end

  # Two profiles of one company sharing a back office would be stamped with one key and
  # start fighting over it.
  def sibling_shares_mid_back_office?(element)
    MboProfile.joins(:tree_node_to_mbo_profiles)
              .where(tree_node_to_mbo_profiles: { tree_node_id: element.company_profiles.select(:id) })
              .where(mid_back_office: element.mid_back_office)
              .where.not(id: element.id)
              .exists?
  end
end