Skip to content

Instantly share code, notes, and snippets.

@Lulu117
Last active April 1, 2017 00:20
Show Gist options
  • Save Lulu117/74d9a2bce26702a1faad5557c61bbda6 to your computer and use it in GitHub Desktop.
Save Lulu117/74d9a2bce26702a1faad5557c61bbda6 to your computer and use it in GitHub Desktop.

State and History

  • Learn how to structure processes as state machines
  • Add an automatic audit trail to ActiveRecord models

So far in our little example app we can buy and sell downloadable products using Stripe. We're not keeping much information in our own database, though. We can't easily see how much we've earned, we can't see how big Stripe's cut has been. Ideally our application's database would keep track of this. The mantra with financial transactions should always be "trust and verify" and to that end we should be tracking sales through each stage of the process, from the point the customer clicks the buy button all the way through to a possible refund. We should know, at any given moment, what state a transaction is in and its entire history.

State Machines

The first step of tracking is to turn each transaction into a state machine. A state machine is simply a formal definition of what states an object can be in and the transitions that can happen to get it between states. At any given moment an object can only be in a single state. For example, consider a subway turnstile. Normally it's locked. When you put a coin in or swipe your card, it unlocks. Then when you pass through it locks itself again.

We can model this turnstile in Ruby using a gem named AASM:

class Turnstile

  include AASM

  aasm do
    state :locked, initial: true
    state :unlocked

    event :pay do
      transitions from: :locked, to: :unlocked
    end

    event :use do
      transitions from: :unlocked, to: :locked
    end
  end
end

As you can see AASM implements a simple DSL for states and events. It will create a few methods on instances of Turnstile, things like pay! and use! to trigger the corresponding events and locked? and unlocked? to ask about the state.

AASM can also be used with ActiveRecord by adding a column to hold the current state. Let's begin by adding some more fields to Sale:

$ rails g migration AddFieldsToSale \
    state:string \
    stripe_id:string \
    stripe_token:string \
    card_expiration:date \
    error:text \
    fee_amount:integer \
    amount:integer
$ rake db:migrate

Now, add aasm to your Gemfile and run bundle install:

gem 'aasm'

The Sale state machine will have four possible states:

  • pending means we just created the record
  • processing means we're in the middle of processing
  • finished means we're done talking to Stripe and everything went well
  • errored means that we're done talking to Stripe and there was an error

It'll also have a few different events for the transaction: process, finish, and fail. Let's describe this using aasm:

class Sale < ActiveRecord::Base
  include AASM

  aasm column: 'state' do
    state :pending, initial: true
    state :processing
    state :finished
    state :errored

    event :process, after: :charge_card do
      transitions from: :pending, to: :processing
    end

    event :finish do
      transitions from: :processing, to: :finished
    end

    event :fail do
      transitions from: :processing, to: :errored
    end
  end

  belongs_to :product

  before_create :populate_guid

  private
  def populate_guid
    self.guid = SecureRandom.uuid()
  end

  def charge_card
    begin
      save!
      charge = Stripe::Charge.create(
        amount: self.amount,
        currency: "usd",
        source: self.stripe_token,
        description: self.email,
      )
      balance = Stripe::BalanceTransaction.retrieve(charge.balance_transaction)
      self.update(
        stripe_id:       charge.id,
        card_expiration: Date.new(charge.source.exp_year, charge.source.exp_month, 1),
        fee_amount:      balance.fee
      )
      self.finish!
    rescue Stripe::StripeError => e
      self.update_attributes(error: e.message)
      self.fail!
    end
  end
end

Inside the aasm block, every state we described earlier gets a state declaration and every event gets an event declaration. Notice that the :pending state is what the record will be created with initially. Also notice that the transition from :pending to :processing has an :after callback declared. After AASM updates the state property and saves the record it will call the charge_card method. AASM will automatically create scopes, so for example you can find how many finished records there are with Sale.finished.count.

We moved the stuff about charging the card into the model which adheres to the Fat Model Skinny Controller principle, where all of the logic lives in the model and the controller just drives it. TransactionsController#create is quite a bit simpler now:

def create
  @product = Product.find_by!(
    permalink: params[:permalink]
  )

  sale = @product.sales.create(
    amount:       @product.price,
    email:        params[:email],
    stripe_token: params[:stripeToken]
  )
  sale.process!
  if sale.finished?
    redirect_to pickup_url(guid: sale.guid)
  else
    flash.now[:alert] = sale.error
    render :new
  end
end

We create the Sale object, and then instead of doing the Stripe processing in the controller we call the process! method that aasm creates. If the sale is finished we'll redirect to the pickup url. If it isn't finished, we assume it's errored so we render out the new view with the error.

It would be nice to see all of this information we're saving now. Let's change the Sales#show template to dump out all of the fields:

<p id="notice"><%= notice %></p>
<table>
  <tr>
    <th>Key</th>
    <th>Value</th>
  </tr>
  <% @sale.attributes.sort.each do |key, value| %>
  <tr>
    <td><%= key %></td>
    <td><%= value %></td>
  </tr>
  <% end %>
</table>

<%= link_to 'Stripe', "https://manage.stripe.com/payments/#{@sale.stripe_id}" %>
<%= link_to 'Back', sales_path %>

Notice that we're deep-linking directly into Stripe's management interface. That will give you one-click access to everything that Stripe knows about this transaction, as well as a button to refund the payment.

Audit Trail

A company I used to work for deals with payments from about eight different payment providers. Each one of them is custom, one-off code that shares very little with the rest of the system. One day we started getting complaints about payments going missing, so started digging. However, not only were we not keeping history in the database, we weren't even comprehensively logging things. It took two software developers almost a week straight to finally figure out how payments were going missing on our end, by cross checking what little information we did have with what the payment providers had. We ended up giving away a bunch of gifts to keep everyone happy, not to mention the opportunity cost of having developers tracking things down. With a proper audit trail on our end we would have instantly been able to see when and where things were getting lost.

There are a few different schools of thought on how to implement audit trails. The classical way would be to use database triggers to write copies of the database rows into an audit table. This has the advantage of working whether you use the ActiveRecord interface or straight SQL queries, but it's really hard to implement properly. Another, easier way, is to hook into your ORM to keep track of things. The easiest way to do this that I've found is to use a gem named Paper Trail. Paper Trail monitors changes on a record using ActiveRecord's life cycle events and will serialize the state of the object before the change and stuff it into a versions table. It has convenient methods for navigating versions, which we'll use to display the history of the record in an admin interface later.

First, add the gem to your Gemfile:

gem 'paper_trail', '~> 3.0.6'

Install the gem, which will generate a migration for you, and run the migration:

$ rails generate paper_trail:install --with-changes
$ rake db:migrate

And now add has_paper_trail to the Sale model:

class Sale < ActiveRecord::Base
  has_paper_trail

  # ... rest of Sale from before
end

has_paper_trail takes a bunch of options for things like specifying which life cycle events to monitor, which fields to include and which to ignore, etc. which are all described in its documentation. The defaults should usually be fine.

Here's some simple code for the SalesController#show action to display the history of the sale. In app/views/sales/show.html.erb:

<table>
  <thead>
    <tr>
      <th>Timestamp</th>
      <th>Event</th>
      <th>Changes</th>
    </tr>
  </thead>
  <tbody>
  <% @sale.versions.each do |version| %>
    <tr>
      <td><%= version.created_at %></td>
      <td><%= version.event %></td>
      <td>
        <% version.changeset.sort.each do |key, value| %>
          <b><%= key %></b>: <%= value[0] %> to <%= value[1] %><br>
        <% end %>
      </td>
    </tr>
  <% end %>
  </tbody>
</table>

And here's what it looks like:

history_table

Each change will have a timestamp, the event, and a block of changes, one row for each column that changed in that update. For a typical completed sale we'll see three rows: record creation, the state change from "pending" to "processing" when the background worker picks the job up, and another row when the background worker updates the Stripe information. The current state table will show the record as "finished". By examining the audit trail for a clean transaction you can do things like get rough performance numbers for your interactions with Stripe, and if you ever have broken transactions you can see when things went wrong and more importantly how things went wrong which will better help you fix them.

Next

In the next chapter we're going to talk about how to handle Stripe's events system, which will call a webhook in your app whenever interesting things happen with your charges, customers, or subscriptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment