In the first post on DRYing interactors we looked at creating modules and shared examples to simplify the process of checking the variables passed to interactors. Now we’ll take things further and add some more complex functionality to a module by handling the process of creating billable items. In addition to adding methods to call from the interactors we’ll also hook in a before block and create a rollback method from the module.

In the app I first wrote this for we have several interactors that calculate billing information on a variety of metrics. Initially all calculations had been done in a single interactor, but the file quickly got too long and did too much. Interactors should do a single unit of work, and the more you follow this rule the happier you’ll be in the long run.

Adding Billable Items

The core of the module we’re setting up is to add a billable item to an array. Later we’ll pass this array to another interactor, which will handle the process of charging for these items. That process will depend on the billing provider you’re using, so we won’t get in to that in this post.

# app/interactors/calculate_usage.rb
class CalculateUsage
  include Interactor
  include BillableItems

  def call
    usage = calcuate_usage

    add_billable_item(
      description: "#{usage} Used",
      amount_in_cents: usage_charge_in_cents(usage),
      type: :minutes_used
    )
  end

private

  def calculate_usage
    ...
  end

  def usage_charge_in_cents(usage)
    ...
  end
end

This interactor calculates usage based on variables that we’re not worried about today. What we’re looking at is how add_billable_item provides a common interface for tracking what we need to bill a customer for as calculated across multiple interactors with our application.

add_billable_item is defined in the BillableItems module which is included near the top of the interactor. I like to keep files like this that are used across multiple interactors in /app/interactors/concerns. As mentioned in part 1, we need to tell our application to load these files by adding

config.autoload_paths += ["#{Rails.root}/app/interactors/concerns"]

to application.rb if we’re building a Rails app.

# app/interactors/concerns/billable_items.rb
module BillableItems

private
  def add_billable_item(description:, amount_in_cents: 0, type: :general)
    context.billable_items << {
      description: description,
      amount_in_cents: amount_in_cents.to_i,
      type: type.to_sym
    }
  end
end

Right now the module just includes a single private method, add_billable_item, that adds a hash to an array. It’s pretty straightforward, but right now it will not run successfully unless we set context.billable_items to an array before trying to add a billable item. We don’t want to have to add this in every interactor that creates a billable item, so instead we’ll set it up in the BillableItems module.

Setting Up The Context in a before block

Interactors allow us to prepare things prior to running in a before hook. This is a good place to set up things like the billable items array we want to add our charges to, but in order to do so from our module we have to approach things a little differently than when we add a method.

# app/interactors/concerns/billable_items.rb
module BillableItems
  def self.included(base)
    base.class_eval do
      before do
        context.billable_items ||= []
      end
    end
  end

private
  ...
end

This new method, self.included, is run when BillableItems is included in a class or module. We then pass in the class that it was included to and run class_eval on it, which lets us dynamically insert the before hook that we want in to the class. This allows us to ensure that the context includes a bilable_items array whenever the BillableItems module is included in any interactor.

Rollback

Interactor rollback is called whenever an organized interactor fails, which gives the opportunity to undo any changes that were made. With our billing scenario we want to remove any billable items that were added by the interactor. We could explicitly add these to each interactor, but of course it’d be ideal to handle them within the BillableItems module instead.

First we need to know what the billable_items array looked like before we added anything to it. The easiest time to set this up is in the before hook that we just set up.

# app/interactors/concerns/billable_items.rb
module BillableItems
  def self.included(base)
    base.class_eval do
      before do
        context.billable_items ||= []
        context.original_billable_items ||= []
        context.original_billable_items << context.billable_items.dup
      end
    end
  end

private
  ...
end

To start off we make sure that there’s an original_billable_items array on the context. This will store the history of the billable_items array as we move through the organizer. Once we know the array exists we take the current billable_items array and duplicate it in to original_billable_items. When we rollback later we’ll just take items out of the array as necessary.

Now that we have our history in place we can define the rollback method.

# app/interactors/concerns/billable_items.rb
module BillableItems
  def self.included(base)
    ...
  end

  def rollback
    context.billable_items = context.original_billable_items.pop
    super
  end

private
  ...
end

rollback starts off by replacing the current billable_items with the last element of original_billable_items and removing it from the array. Then we call super in case the interactor itself has a rollback method that needs to perform any additional cleanup.

The Full BillableItems Module

We’ve only looked at parts of the module as we’ve stepped through the pieces, so here’s what the whole thing looks like:

# app/interactors/concerns/billable_items.rb
module BillableItems
  def self.included(base)
    base.class_eval do
      before do
        context.billable_items ||= []
        context.original_billable_items ||= []
        context.original_billable_items << context.billable_items.dup
      end
    end
  end

  def rollback
    context.billable_items = context.original_billable_items.pop
    super
  end

private

  def add_billable_item(description:, amount_in_cents: 0, type: :general)
    context.billable_items << {
      description: description,
      amount_in_cents: amount_in_cents.to_i,
      type: type.to_sym
    }
  end
end

In my real world usage I also have methods to calculate the total charges contained in the billable_items array, which are run in rollback and add_billable_items. This simplifies the process of getting the total charges and keeps the logic around billable items all in one location.

Tests

Last but not least (and they probably should’ve been first) are the tests. As in part 1, RSpec’s shared examples play a big part in keeping tests clean and repeatable.

# spec/interactors/calculate_usage_spec.rb
require "rails_helper"

describe CalculateUsage do
  subject) do
    CalculateUsage.call(
      account: account,
      billable_items: billable_items
    )
  end

  let(:billable_items){ [] }
  let(:account){ double("Account", minutes_used: minutes_used) }
  let(:minutes_used){ 0 }

  describe ".call" do
    context "with all params" do
      let(:minutes_used){ 5 }

      it_behaves_like(
        "it has billable items of :type totaling :amount_in_cents",
        :minutes_used,
        20
      )

      it_behaves_like(
        "it has billable items of :type matching :description",
        :minutes_used,
        /5 Used/i
      )
    end
  end
end

By using shared examples we have an easily reusable set of tests that confirm that our calculations and billable item descriptions are being generated as expected.

# spec/support/shared_examples_for_interactors_with_billable_items.rb
RSpec.shared_examples "it has billable items of :type totaling :amount_in_cents" do |type, amount|
  it "has billable items of #{type} totaling #{amount}"do
    expect(
      subject.billable_items.find_all{ |x| x[:type] == type }.sum{ |x| x[:amount_in_cents] }
    ).to eq(amount)
  end
end

RSpec.shared_examples "it has billable items of :type matching :description" do |type, description|
  it "has billable items of #{type} matching #{description}" do
    expect(
      subject.billable_items.find_all{ |x| x[:type] == type }.map{ |x| x[:description] }
    ).to include(description)
  end
end

Our shared examples here vary from what we used in part 1 because they are receiving variables that we’re using to populate the tests. This allows us the flexibility of testing for the actual values we expect, without requiring every interactor that involves billing calculations to get in to the nitty gritty of how a billable_item is structured.

Since we’re also managing part of the rollback process from the module, we should also test that with a shared example.

# spec/support/shared_examples_for_interactors_with_billable_items.rb
RSpec.shared_examples "billable items are rolled back" do
  it "includes the BillableItems module" do
    expect(described_class.included_modules.include?(BillableItems))
      .to be true
  end

  context "billable items are rolled back" do
    before{ interactor_parameters ||= {} }

    let(:interactor)do
      described_class.new(
        {
          billable_items: billable_items,
          original_billable_items: original_billable_items
        }.merge(
          interactor_parameters
        )
      )
    end

    let(:billable_items){ [{ description: "x", amount_in_cents: 200 }] }
    let(:original_billable_items) do
      [[{ description: "x", amount_in_cents: 300 }]]
    end

    let(:context){ interactor.context }

    it "adds a billable item" do
      expect{ interactor.call }.to change{ context.billable_items.size }.by(1)
    end

    it "reverts to the original billable items" do
      interactor.call
      expect{ interactor.rollback }.to(
        change{ context.billable_items }.to(original_billable_items.first)
      )
    end
  end
end

In addition to making sure that the BillableItems module is included in the class, we test that rollback works as expected. Since our tests don’t run the interactors in an organizer we need to build up the original_billable_items array by hand, then make sure that adding a new billable item grows the history array, and srhinks it back down after rollback.

Wrapping Up

A big part of the power of interactors is their ability to simplify and compartmentalize business logic in our applications. Moving common functionality, and the tests that deal with that functionality, out in to modules and shared examples helps us keep interactors simple, consistent, and understandable, without sacrificing capability.