Maintenance::ResyncAgentPortMboProfilesTask

Source code
# frozen_string_literal: true

##
# Re-runs the AgentPort MBO sync for companies still carrying a wrongly-classified
# AgentPort MBO profile.
#
# mid_back_office_from_reporting_type previously never resolved a CompanyReportingInfo
# reporting_type, so every profile synced through that path was silently assigned the
# country's default back office instead of the one AgentPort reported. Resolution is
# correct going forward, but existing wrong profiles only self-correct on their next
# sync — Webhook::Adapters::AgentPortSyncAdapter does not fire on MBO-only edits. This
# task forces that next sync for every company still affected.
#
# Two populations were left wrong by the bug:
#   1. Profiles predating slot-based provenance (agent_port_source: nil) — the same
#      population BackfillMboProfileAgentPortSourceTask targets.
#   2. Profiles synced AFTER provenance was introduced but before the regex fix: the
#      slot key (AgentPort::MboSourceKey) is computed independently of
#      mid_back_office resolution, so these already carry a correct CRI:U44/U52/U53
#      agent_port_source alongside a wrong (country-default) mid_back_office.
#
# A profile shared with another company is skipped rather than resynced: the resolver
# only forks a shared record when mbo_id changes (ModelAssociationService::
# MboProfileResolver#mbo_id_change?), and this correction changes mid_back_office only,
# so an in-place fix would silently rewrite the classification for every company
# sharing that record. Those need manual review, not an automated resync.
class Maintenance::ResyncAgentPortMboProfilesTask < MaintenanceTasks::Task
  # AgentPort::MboSourceKey format for CompanyReportingInfo slots, mapped to the
  # mid_back_office they should resolve to — mirrors
  # AgentPort::ModelProcessors::MboProfilesModelProcessor::DEFAULT_MBO_MAPPING.
  CRI_SLOT_TO_MID_BACK_OFFICE = {
    'CRI:U44' => MboProfiles::MidBackOffice::LUMINA,
    'CRI:U52' => MboProfiles::MidBackOffice::PROTAS,
    'CRI:U53' => MboProfiles::MidBackOffice::D365
  }.freeze

  collection_batch_size(100)

  def collection
    CompanyProfile.where(id: affected_company_profile_ids).where.not(company_guid: nil)
  end

  def process(company_profile)
    AgentPortSyncJob.perform_later(company_profile.id, [:mbo_profiles])
  rescue StandardError => e
    Rails.logger.error("Failed to enqueue AgentPort MBO resync for CompanyProfile #{company_profile.id}: #{e.message}")
  end

  private

  def affected_company_profile_ids
    CompanyProfileToMboProfile
      .joins(:mbo_profile)
      .merge(unshared_affected_profiles)
      .distinct
      .select(:tree_node_id)
  end

  def unshared_affected_profiles
    MboProfile
      .where(sync_data_source: MboProfiles::SyncDataSource::AGENT_PORT)
      .merge(mismatched_scope)
      .where.not(id: shared_profile_ids)
  end

  # Matches the exact slot key or one of its duplicate-occurrence variants: a second
  # CompanyReportingInfo U44 row is keyed "CRI:U44#1", not "CRI:U44" — see
  # AgentPort::MboSourceKey. The '#' anchor avoids a false match on an unrelated slot that
  # merely starts with the same digits (e.g. a hypothetical "U440" type).
  def mismatched_scope
    no_provenance = MboProfile.where(agent_port_source: nil)
    CRI_SLOT_TO_MID_BACK_OFFICE.reduce(no_provenance) do |scope, (slot, correct_mbo)|
      scope.or(
        MboProfile.where(agent_port_source: slot)
                  .or(MboProfile.where('agent_port_source LIKE ?', "#{slot}\\#%"))
                  .where.not(mid_back_office: correct_mbo)
      )
    end
  end

  # Profiles linked to more than one company profile: correcting mid_back_office in
  # place would change the classification for every company sharing the record.
  def shared_profile_ids
    CompanyProfileToMboProfile
      .group(:mbo_profile_id)
      .having('COUNT(DISTINCT tree_node_id) > 1')
      .select(:mbo_profile_id)
  end
end

Previous Runs

Succeeded

Processed 1,593 out of 1,593 items (100%).

Ran for less than 5 seconds, finished .

Metadata:
user_id
-1