Active Record has a nice feature that enables the programmer to separate concerns. In the application I am building, I need to reject some transactions based on business rules. For example, if an attacker attemps to empty an account by requesting multiple payouts, or attempting to load more than a specific amount of money in the user’s account.
In this application, I am using STI (Single Table Inheritance) to represent the different types of transaction. The hierarchy looks like this:
1 class GreenbackTransaction < ActiveRecord::Base 2 end 3 4 class AccountRechargeTransaction < GreenbackTransaction 5 end 6 7 class AccountPayoutTransaction < GreenbackTransaction 8 end
I defined my observer like this:
app/models/greenback_transaction_observer.rb
1 class GreenbackTransactionObserver < ActiveRecord::Observer 2 def after_create(txn) 3 # Code to reject the transaction if it is suspicious 4 end 5 end
Using the console, everything was working fine. So, off I went to write a test for it (I know, it should have been the other way around, but I haven’t used observers much, and I wanted to see what was going on).
My test is defined like this:
test/unit/greenback_transaction_observer_test.rb
1 class GreenbackTransactionObserverTest < Test::Unit::TestCase 2 fixtures :greenback_transactions, :users, :accounts, :affected_accounts 3 4 def setup 5 @sam = users(:sam) 6 Setting.recharge_amount_threshold = 150.to_money 7 end 8 9 def test_suspicious_transaction_rejected 10 assert_nothing_raised do 11 AccountRechargeTransaction.new(:account => @sam.account, :amount => 100.to_money) 12 end 13 14 assert_raise(TransactionFailureException) do 15 AccountRechargeTransaction.new(:account => @sam.account, :amount => 100.to_money) 16 end 17 end 18 end
To my complete surprise, this didn’t work. After much investigation, I found that the observer was not loaded for GreenbackTransaction. After some fooling around, adding logging statements in Rails core, I finally stumbled upon the solution:
app/models/account_recharge_transaction_observer.rb
1 class AccountRechargeTransactionObserver < ActiveRecord::Observer 2 observe AccountRechargeTransaction 3 4 def after_create(txn) 5 # Code to reject the transaction if it is suspicious 6 end 7 end
The problem was the observer was registered on GreenbackTransaction, and it seems the observers aren’t inherited in subclasses. This is important and bears repeating: if you use STI and observers, observe your subclasses !.