Class: Maintenance::SourceAttributionBackfill

Inherits:
BaseService
  • Object
show all
Defined in:
app/services/maintenance/source_attribution_backfill.rb

Overview

Service object: evidence-based backfill of polluted source attribution.

Phase 0.5 of the source-attribution rework
(doc/tasks/202607231507_SOURCE_ATTRIBUTION_MODEL_RESTRUCTURE.md, Basecamp
10014579025). Scope decided 2026-07-23: all-time window; a record
contradicts its visit only when its source neither equals nor descends
from the visit's source and no human re-sourced it afterwards; order
corrections cascade to invoices. Trade-show records with no evidence at
all (Menards-class account inheritance) are deliberately NOT restored
here — they get platform sources in Phase 2 so they land once, correctly.

Every tier defaults to dry_run: true and only reports what it would
change. Live runs are gated behind the bulk-operation protocol (count
first, two confirmations). Writes use update_column — deliberately no
callbacks (an Order save would trigger recalculation side effects) —
with every change logged and returned for archival.

Defined Under Namespace

Classes: Result

Constant Summary collapse

TRADE_SHOW_ROOT_ID =

Root of the trade-show source subtree (Source id) whose re-tags Tier 1 restores.

847
SAMPLE_LIMIT =

Changes returned per tier in the summary (all changes are logged).

100

Instance Attribute Summary

Attributes inherited from BaseService

#options

Instance Method Summary collapse

Methods inherited from BaseService

#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #tagged_logger

Constructor Details

This class inherits a constructor from BaseService

Instance Method Details

#processHash{Symbol => Result}

Dry-runs every tier and returns the combined summary.

Returns:

  • (Hash{Symbol => Result})

    per-tier results



31
32
33
34
35
36
37
38
# File 'app/services/maintenance/source_attribution_backfill.rb', line 31

def process
  {
    trade_show_retags: restore_trade_show_retags,
    visit_contradicted: restore_visit_contradicted,
    invalid_campaign_tags: restore_invalid_campaign_tags,
    account_sources: 
  }
end

#restore_account_sources(dry_run: true) ⇒ Result

Tier 4: accounts whose current source drifted from the immutable
original into the trade-show subtree or a campaign-generated source.
Restores customer.source to customer.original_source. Narrowings
(current descends from original) and unknown originals are kept.

Parameters:

  • dry_run (Boolean) (defaults to: true)

Returns:



124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'app/services/maintenance/source_attribution_backfill.rb', line 124

def (dry_run: true)
  run_tier(:account_sources, dry_run:) do |result|
    polluted_ids = trade_show_source_ids + campaigns_by_source_id.keys
    Customer.where(source_id: polluted_ids)
            .where.not(original_source_id: [nil, Source::UNKNOWN_ID])
            .where(Customer.arel_table[:source_id].is_distinct_from(Customer.arel_table[:original_source_id]))
            .find_each do |customer|
      next if descendant?(customer.source_id, customer.original_source_id)

      apply_change(result, customer, customer.original_source_id, dry_run:, reason: 'restore original source', cascade: false)
    end
  end
end

#restore_invalid_campaign_tags(dry_run: true) ⇒ Result

Tier 3: campaign-generated sources on records of customers who never
joined that campaign's audience, or whose record predates their join
(pre-touch tagging, e.g. SO720065). Restores the audit-trail prior
value, else visit evidence, else the customer's original source;
otherwise reports the record as unrestorable.

Parameters:

  • dry_run (Boolean) (defaults to: true)

Returns:



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'app/services/maintenance/source_attribution_backfill.rb', line 97

def restore_invalid_campaign_tags(dry_run: true)
  run_tier(:invalid_campaign_tags, dry_run:) do |result|
    [Opportunity.all, Order.where.not(state: 'cart')].each do |scope|
      scope.where(source_id: campaigns_by_source_id.keys).includes(:visit).find_each do |record|
        campaign_id = campaigns_by_source_id[record.source_id]
        joined_at = audience_join_time(campaign_id, record.customer_id)
        next unless joined_at.nil? || record.created_at < joined_at

        target = restoration_target(record)
        if target && target != record.source_id
          reason = joined_at.nil? ? 'never in campaign audience' : 'created before audience join'
          apply_change(result, record, target, dry_run:, reason:)
        else
          result[:unrestorable] << record_key(record)
        end
      end
    end
  end
end

#restore_trade_show_retags(dry_run: true) ⇒ Result

Tier 1: human re-tags to trade-show sources, restorable from the audit
trail. For each record whose current source is still the re-tagged
trade-show value, restore the value it had before the first re-tag
into the trade-show subtree (e.g. SO722207's opportunity:
"Nurturing Campaign" → "IBS 2026" a week after the show → restored).

Parameters:

  • dry_run (Boolean) (defaults to: true)

Returns:



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'app/services/maintenance/source_attribution_backfill.rb', line 48

def restore_trade_show_retags(dry_run: true)
  run_tier(:trade_show_retags, dry_run:) do |result|
    trade_show_retag_versions.group_by { |v| [v.item_type, v.item_id] }.each do |(item_type, item_id), versions|
      first_retag = versions.min_by(&:created_at)
      prior_id, retag_id = source_change(first_retag)
      next if prior_id.nil? || trade_show_source_ids.include?(prior_id)

      record = item_type.constantize.find_by(id: item_id)
      # current must still equal the re-tagged value — a later deliberate
      # re-source (even to another trade-show node) is kept
      next unless record && record.source_id == retag_id

      apply_change(result, record, prior_id, dry_run:, reason: "retagged #{prior_id}#{retag_id} by #{first_retag.whodunnit}")
    end
  end
end

#restore_visit_contradicted(dry_run: true) ⇒ Result

Tier 2: records contradicting their own visit's source. Skips
narrowings (record source descends from the visit source, e.g.
Google → Google Ads) and records a human re-sourced after creation.

Parameters:

  • dry_run (Boolean) (defaults to: true)

Returns:



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'app/services/maintenance/source_attribution_backfill.rb', line 71

def restore_visit_contradicted(dry_run: true)
  run_tier(:visit_contradicted, dry_run:) do |result|
    [Opportunity.all, Order.where.not(state: 'cart')].each do |scope|
      human_touched = human_retouched_ids(scope.klass.name)
      scope.joins(:visit)
           .where.not(visits: { source_id: nil })
           .where(scope.klass.arel_table[:source_id].is_distinct_from(Visit.arel_table[:source_id]))
           .includes(:visit)
           .find_each do |record|
        next if human_touched.include?(record.id)
        next if descendant?(record.source_id, record.visit.source_id)

        apply_change(result, record, record.visit.source_id, dry_run:, reason: 'visit evidence')
      end
    end
  end
end