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</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> =&gt; <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 !.

Search

Your Host

A picture of me

I am François Beausoleil, a Ruby on Rails and Scala developer. During the day, I work on Seevibes, a platform to measure social interactions related to TV shows. At night, I am interested many things. Read my biography.

Top Tags

Books I read and recommend

Links

Projects I work on

Projects I worked on