Class: Maintenance::SourceAttributionBackfill
- Inherits:
-
BaseService
- Object
- BaseService
- Maintenance::SourceAttributionBackfill
- 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
Instance Method Summary collapse
-
#process ⇒ Hash{Symbol => Result}
Dry-runs every tier and returns the combined summary.
-
#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.
-
#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).
-
#restore_trade_show_retags(dry_run: true) ⇒ Result
Tier 1: human re-tags to trade-show sources, restorable from the audit trail.
-
#restore_visit_contradicted(dry_run: true) ⇒ Result
Tier 2: records contradicting their own visit's source.
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
#process ⇒ Hash{Symbol => Result}
Dry-runs every tier and returns the combined summary.
31 32 33 34 35 36 37 38 |
# File 'app/services/maintenance/source_attribution_backfill.rb', line 31 def process { trade_show_retags: , visit_contradicted: restore_visit_contradicted, invalid_campaign_tags: , account_sources: restore_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.
124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'app/services/maintenance/source_attribution_backfill.rb', line 124 def restore_account_sources(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.
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 (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).
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 (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.
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 |