armandofox/audience1st

View on GitHub
app/models/customer/merge.rb

Summary

Maintainability
A
2 hrs
Test Coverage
A
96%
class Customer < ActiveRecord::Base
  
  # merge myself with another customer.  'params' array indicates which
  # record (self or other) to retain each field value from.  For
  # password and salt, the ones corresponding to most recent
  # last_login are retained.  If those are equal, keep whichever was
  # most recently updated (updated_at).  IF those are also equal, keep
  # the first one.

  # merge with Anonymous customer, keeping all transactions

  def forget!
    return nil unless deletable?
    begin
      transaction do
        Customer.update_foreign_keys_from_to(self.id, Customer.anonymous_customer.id)
        self.destroy
      end
    rescue StandardError => e
      self.errors.add :base,"Cannot forget customer #{id} (#{full_name}): #{e.message}"
    end
    return self.errors.empty?
  end

  def merge_with_params!(c1,params)
    return nil unless self.mergeable_with?(c1)
    Customer.replaceable_attributes.each do |attr|
      if (params[attr.to_sym].to_i > 0)
        self.send("#{attr}=", c1.send(attr))
      end
    end
    finish_merge(c1)
    return Customer.save_and_update_foreign_keys!(self, c1)
  end
  
  def merge_automatically!(c1)
    return nil unless self.mergeable_with?(c1)
    replace = c1.fresher_than?(self) && !c1.created_by_admin?
    Customer.replaceable_attributes.each do |attr|
      self.send("#{attr}=", c1.send(attr)) if replace || self.send(attr).blank?
    end
    finish_merge(c1)
    return Customer.save_and_update_foreign_keys!(self, c1)
  end
        
  def mergeable_with?(other)
    if other.special_customer? || self.is_admin || other.is_admin
      self.errors.add :base, I18n.translate('customers.errors.merge.admin')
    elsif (self.special_customer? && self != Customer.anonymous_customer)
      self.errors.add :base, I18n.translate('customers.errors.merge.special')
    end
    self.errors.empty?
  end
  

  def fresher_than?(other)
    begin
      (self.updated_at > other.updated_at) ||
        (self.updated_at == other.updated_at &&
        self.last_login > other.last_login)
    rescue
      nil
    end
  end

  private

  def finish_merge(c1)
    %w(comments tags role blacklist e_blacklist created_by_admin).each do |attr|
      newval = merge_attribute(c1, attr)
      self.send("#{attr}=", newval)
    end
  end

  def merge_attribute(other, attr)
    v1 = self.send(attr)
    v2 = other.send(attr)
    newval =
      case attr.to_sym
      when :comments then [v1,v2].reject { |c| c.blank? }.join('; ')
      when :tags then (v1.to_s.downcase.split(/\s+/)+v2.to_s.downcase.split(/\s+/)).uniq.join(' ')
      when :role then [v1.to_i, v2.to_i].max  
      when :blacklist, :e_blacklist  then v1 || v2
      when :created_by_admin, :inactive then v1 && v2
      else raise "No automatic merge procedure for #{attr.to_s.humanize}"
      end
  end

  # Note: This method should only be called inside a transaction block!
  def self.update_foreign_keys_from_to(old,new)
    msg = []
    [Order, Item, Txn].each do |t|
      howmany = 0
      t.foreign_keys_to_customer.each do |field|
        howmany += t.where("#{field} = ?", old).update_all(field => new)
      end
      msg << "#{howmany} #{t}s"
    end
    msg
  end

  def self.save_and_update_foreign_keys!(c0,c1)
    new = c0.id
    old = c1.id
    ok = nil
    msg = []
    begin
      transaction do
        msg = Customer.update_foreign_keys_from_to(old, new)
        # Add/merge labels from old record to new
        label_count = (c1.labels - c0.labels).size
        c0.labels += c1.labels
        msg << "#{label_count} labels"
        # Definitively propagate provenance (either ticket_sales_import_id, or nil)
        # from the older record.
        if c0.created_at > c1.created_at # c1 is older,but c0 will be result of merge
          c0.ticket_sales_import_id = c1.ticket_sales_import_id
        end
        # Crypted_password and salt have to be updated separately,
        # since crypted_password is automatically set by the before-save
        # action to be encrypted with salt.
        if c1.fresher_than?(c0)
          pass = c1.crypted_password
          salt = c1.salt
        else
          pass = nil
        end
        c1.destroy
        c0.save!
        if pass
          Customer.connection.execute("UPDATE customers SET crypted_password='#{pass}',salt='#{salt}' WHERE id=#{c0.id}")
        end
        ok = "Transferred " + msg.join(", ") + " to customer id #{new}"
      end
    rescue StandardError => e
      c0.errors.add :base,"Customers NOT merged: #{e.message}"
    end
    return ok
  end

end