Guido
Guido

Reputation: 387

Run before_save after changed in attribute in a related model

I am having some troubles today and cannot think outside the box to fix this one.

Basically I have a model called Airplane, which has_many Payments. Each payment can be divided in many Installments. Ok!

Here is the info:

Model Airplane
- has_many payments
- before_save :checks_if_everything_has_been_paid

Model Payment
- belongs_to airplane
- has_many installments

Model Installment
- belongs_to payment

So, what I want to do is when the sum of the installments be equal or greater than the Airplane ticket value then the Airplane.paid will be true. I am doing that using the before_save "checks_if_everything_has_been_paid. But it only works when there are changes on the Airplane fields.

How can I run this class when there are changes both in the Payment and Installment fields?

I want to check if the payment is completed everytime an installment is changed or the Payment itself.

Thank you!

Upvotes: 3

Views: 2319

Answers (2)

infused
infused

Reputation: 24337

Instead of defining an after save callback on the Airplane model, define a after_add callback on the payments association.

class Airplane < ActiveRecord::Base
  has_many :payments, after_add: :checks_if_everything_has_been_paid

  def checks_if_everything_has_been_paid
    # work some magic
  end
end

Update: I think the following may be a better approach if I understand your data model correctly. If a payment or installment is saved it will trigger the airplane to check for full payment:

class Airplane < ActiveRecord::Base
  has_many :payments
  has_many :installments, through: :payments

  def check_for_full_payment
    # work some magic
  end
end

class Payment < ActiveRecord::Base
  belongs_to :airplane
  has_many :installments

  after_save :trigger_full_payment_check

  def trigger_payments_check
    airplane.check_for_full_payment
  end
end

class Installment < ActiveRecord::Base
  belongs_to :payment

  delegate :airplane, to: :payment

  after_save :trigger_full_payment_check

  def trigger_payments_check
    airplane.check_for_full_payment
  end
end

The nice thing about this approach is that the logic in Payment and Installment is identical, so you can extract it to a module:

module TriggerFullPaymentCheck
  def self.included(base)
    base.after_save :trigger_full_payment_check
  end

  def trigger_payments_check
    airplane.check_for_full_payment
  end
end

class Airplane < ActiveRecord::Base
  has_many :payments
  has_many :installments, through: :payments

  def check_for_full_payment
    # work some magic
  end
end

class Payment < ActiveRecord::Base
  include TriggerFullPaymentCheck

  belongs_to :airplane
  has_many :installments
end

class Installment < ActiveRecord::Base
  include TriggerFullPaymentCheck

  belongs_to :payment
  delegate :airplane, to: :payment
end

Upvotes: 2

James Mason
James Mason

Reputation: 4296

You can try setting :autosave on your belongs_to associations. If that works, it's a clever solution :).

A better option is probably to create an after_save hook that ask Airplane to check itself.

class Airplane
  has_many :payments

  def check_for_paid
    # use payments(true) to force the association to reload
    if payments(true).all?(&:paid?)
      paid = true
      save
    end
  end
end

class Payment
  # associations

  def paid?
    installments(true).all?(&:paid?)
  end
end

class Installment
  after_save :check_for_paid

  def check_for_paid
    payment.airplane.check_for_paid
  end
end

This way it's more explicit what's happening and why. The other would definitely be more clever, though.

Upvotes: 0

Related Questions