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
5sam</span> = users(<span class="sy">:sam</span>) <span class="no"> 6</span> <span class="co">Setting</span>.recharge_amount_threshold = <span class="i">150</span>.to_money <span class="no"> 7</span> <span class="r">end</span> <span class="no"> 8</span> <span class="no"> 9</span> <span class="r">def</span> <span class="fu">test_suspicious_transaction_rejected</span> <span class="no"><strong>10</strong></span> assert_nothing_raised <span class="r">do</span> <span class="no">11</span> <span class="co">AccountRechargeTransaction</span>.new(<span class="sy">:account</span> => <span class="iv">
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 !.