Class: Effective::Merge

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Model
Defined in:
app/models/effective/merge.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#current_userObject

Returns the value of attribute current_user.



5
6
7
# File 'app/models/effective/merge.rb', line 5

def current_user
  @current_user
end

#source_idObject

Returns the value of attribute source_id.



6
7
8
# File 'app/models/effective/merge.rb', line 6

def source_id
  @source_id
end

#source_typeObject

Returns the value of attribute source_type.



6
7
8
# File 'app/models/effective/merge.rb', line 6

def source_type
  @source_type
end

#target_idObject

Returns the value of attribute target_id.



7
8
9
# File 'app/models/effective/merge.rb', line 7

def target_id
  @target_id
end

#target_typeObject

Returns the value of attribute target_type.



7
8
9
# File 'app/models/effective/merge.rb', line 7

def target_type
  @target_type
end

Instance Method Details

#merge!(validate: true) ⇒ Object

Raises:

  • (ActiveRecord::RecordInvalid)


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'app/models/effective/merge.rb', line 61

def merge!(validate: true)
  raise ActiveRecord::RecordInvalid.new(self) unless valid?

  Rails.application.eager_load! unless Rails.application.config.eager_load

  klasses = defined?(Tenant) ? Tenant.klasses : ActiveRecord::Base.descendants.reject(&:abstract_class?)
  klasses = klasses.select { |klass| klass.table_exists? }

  success = false

  EffectiveResources.transaction do
    # Re-point every record that belongs to the source onto the target, treating the target as the
    # authoritative account. We walk from the belongs_to side so we catch foreign keys no has_many/has_one
    # is declared for - polymorphic owners (Effective::Address, Effective::EventRegistration) and named
    # self-refs alike (advisor_id, endorser_id, reviewer_id) - and move them with update_all. No per-record
    # validations or callbacks run, so historical data and business rules can't block the merge. Any source
    # record that would duplicate one the target already owns (per a uniqueness validator OR a unique index)
    # is deleted instead of moved, so the merge never creates a duplicate or trips a unique constraint.
    klasses.each do |klass|
      source_foreign_keys(klass).each do |foreign_key, foreign_type|
        source_records = records_for(klass, foreign_key, foreign_type, source)
        next unless source_records.exists?

        attributes = { foreign_key => target.id }
        attributes[foreign_type] = target.class.name if foreign_type

        # Addresses are kept as a whole set, not merged record by record: if the target already has any
        # addresses, keep the target's and drop the source's; only copy the source's over when the target
        # has none.
        if klass.name == 'Effective::Address'
          records_for(klass, foreign_key, foreign_type, target).exists? ? source_records.delete_all : source_records.update_all(attributes)
          next
        end

        duplicate_ids = duplicate_record_ids(klass, foreign_key, foreign_type)
        source_records.where(id: duplicate_ids).delete_all if duplicate_ids.present?
        source_records.where.not(id: duplicate_ids).update_all(attributes)
      end
    end

    # Prove the merge is complete before we destroy the source: nothing may still reference it.
    assert_no_references_to_source!(klasses)

    # Everything the source owned now points at the target; whatever is left dies with the source.
    # Reload first so dependent: callbacks only fire for what STILL points at the source (update_all
    # bypasses the in-memory association cache).
    source.reload.destroy!
    target.save!(validate: validate)

    log_merged!

    success = true
  end

  success
end

#new_record?Boolean

Returns:

  • (Boolean)


53
54
55
# File 'app/models/effective/merge.rb', line 53

def new_record?
  true
end

#save!Object



57
58
59
# File 'app/models/effective/merge.rb', line 57

def save!
  merge!
end

#sourceObject



33
34
35
# File 'app/models/effective/merge.rb', line 33

def source
  @source ||= source_type.try(:safe_constantize).try(:find_by_id, source_id)
end

#source=(resource) ⇒ Object



37
38
39
40
41
# File 'app/models/effective/merge.rb', line 37

def source=(resource)
  raise('expected an ActiveRecord::Base resource') unless resource.is_a?(ActiveRecord::Base)
  assign_attributes(source_type: resource.class.name, source_id: resource.id)
  @source = resource
end

#targetObject



43
44
45
# File 'app/models/effective/merge.rb', line 43

def target
  @target ||= target_type.try(:safe_constantize).try(:find_by_id, target_id)
end

#target=(resource) ⇒ Object



47
48
49
50
51
# File 'app/models/effective/merge.rb', line 47

def target=(resource)
  raise('expected an ActiveRecord::Base resource') unless resource.is_a?(ActiveRecord::Base)
  assign_attributes(target_type: resource.class.name, target_id: resource.id)
  @target = resource
end

#to_sObject



29
30
31
# File 'app/models/effective/merge.rb', line 29

def to_s
  (source.present? && target.present?) ? "Merge of #{source} to #{target}" : "New Merge"
end