# Rails 5.2 callbacks to trigger score recalculation for relationships

I have a model that is in a HABTM relationship with 2 other models:

Activity.rb

  has_and_belongs_to_many :mandates, -> { distinct }
has_and_belongs_to_many :contacts, -> { distinct }


I had to add the following callbacks, since the models defined above have a 'score algorithm' that must be recomputed if activities are removed completely, or one is created initially:

  after_commit  :increase_contact_scores, :increase_mandate_scores
after_destroy :decrease_contact_scores, :decrease_mandate_scores


The callbacks code seems horribly the same. Is there a way to simplify this by somehow passing the HABT relation class from the callback to a more 'generic' method that can replace these?

I also need to figure out how to ONLY trigger the relevant callback based on the type of the HABTM (ie. not call both increases and both decreases, since if a mandate is destroyed, calling the decrease_contacts_score method will fail).

  def increase_contact_scores
contacts.each do |contact|
if contact.activities.count == 1
contact.calculate_score
contact.save!
end
end
end

def increase_mandate_scores
mandates.each do |mandate|
if mandate.activities.count == 1
mandate.calculate_score
mandate.save!
end
end
end

def decrease_contact_scores
contacts.each do |contact|
if mandate.activities.count.zero?
contact.calculate_score
contact.save!
end
end
end

def decrease_mandate_scores
mandates.each do |mandate|
if mandate.activities.count.zero?
mandate.calculate_score
mandate.save!
end
end
end


In testing this code it turned out there is a chicken-and-egg situation, and more specific requirements.

In order to know which object (Contact or Mandate) to update the score for, the Activity callbacks need more 'knowledge':

eg. 2 cases:

1 Contact A has 0 activities ->

if an activity is created, recalculate the score for A

2 Contact A has > 0 activities ->

if an activity is created, no need to recalculate the score

if an activity is deleted, recalculate the score only if activities = 0

The same is true for Mandate.

So, I ended up with a cleaner solution, with drier code:

Activity.rb

  before_commit :mark_objects_for_rescoring, on: :create
before_destroy :mark_objects_for_rescoring
after_commit :rescore_objects, on: [:create, :destroy]

after_initialize do
self.contacts_to_recalculate = []
end

private

def mark_objects_for_rescoring
contacts.each do |contact|
contacts_to_recalculate << contact.id if contact.activities.count == 1
end
mandates.each do |mandate|
mandates_to_recalculate << mandate.id if mandate.activities.count == 1
end
end

def rescore_objects
contacts_to_recalculate.each do |id|
contact = Contact.find(id)
contact.calculate_score
contact.save!
end
contacts_to_recalculate.each do |id|
mandate = Contact.find(id)
mandate.calculate_score
mandate.save!
end
end