Blog icon

Managing Stripe subscription payments in Rails

By Victor Hazbun,
Victor Hazbun
Scroll down to read

Some theory before the action

Stripe has a great API to manage subscription payments. Here we take advantage of it to implement recurring subscriptions in Rails 5.

Using the Stripe API means we do not have to store sensitive customer information like (credit card number or CVC), and the APIs are already set up to handle complex cases such as update plans, manage subscriptions, trigger refunds, and more.

We will set up the Stripe API to handle our subscriptions. We also need Stripe to tell us of ongoing payments and the failure of ongoing payments. This will be possible through webhooks, which are endpoints on our application that Stripe will use to send us details of transactions when changes happen via Stripe.

Our TO-DO list

  • We'll create plans locally and on Stripe
  • We'll list our plans and select one of them
  • We'll subscribe to a selected plan
  • We'll implement Stripe webhooks to listen and register events locally

NOTE: We'll create plans, subscriptions and customers locally since we need to have that data in our application we also want to send that data to Stripe so we can manage it through the Stripe API. NOTE: It's important to state that we are not going to store any credit card information in our systems.

Let's get started with the code

Generating the Plan model

Let's create the plans table and model. Feel free to add more columns to match your own business requirements.

rails generate model plan payment_gateway_plan_identifier:string name:string \
price:monetize interval:integer interval_count:integer \
status:integer description:text

Plan model

app/models/plan.rb

class Plan < ApplicationRecord
  enum status: {inactive: 0, active: 1}
  enum interval: {day: 0, week: 1, month: 2, year: 3}

  monetize :price_cents

  def end_date_from(date = nil)
    date ||= Date.current.to_date
    interval_count.send(interval).from_now(date)
  end
end

Stripe customers

Let's add the Stripe customer id to our users table. We need a Stripe customer in order to associate it to a Stripe plan.

NOTE: I assume that you have a User model already in your application

class AddPaymentGatewayCustomerIdentifierToUser < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :payment_gateway_customer_identifier, :string
  end
end

Subscription model

We need a Subscription model in order to track subscriptions locally. Of course, we will also create those subscriptions on Stripe.

rails generate model subscription user:references \
plan:references start_date:date end_date:date \
status:integer payment_gateway:string payment_gateway_subscription_id:string

app/models/subscription.rb

class Subscription < ApplicationRecord
  belongs_to :user
  belongs_to :plan

  enum status: {active: 0, inactive: 1, canceled: 2}
end

Plans controller

In order to subscribe to a plan, we need to list all the active plans, here is the controller.

app/controllers/plans_controller.rb

class PlansController < ApplicationController
  def index
    @plans = Plan.active
    fresh_when(@plans)
  end
end

Select a plan

Display all the plans information and a link to the subscription page.

app/views/plans/index.html.erb

<h2>Plans</h2>
<% @plans.each do |plan| %>
  <h4><%= plan.name %></h4>
  <p><%= plan.description %></p>
  <p><%= humanized_money_with_symbol(plan.price) %></p>
  <%= link_to("Subscribe to #{plan.name.titleize} Plan", new_plan_subscription_path(plan)) %>
<% end %>

Subscribe form

Display a form with credit card details information like Card number, CVC, Expiration Month and Year. Using Stripe JS will allow us to get the payment errors (if any).

app/views/subscriptions/new.html.erb

<%= form_tag subscription_path, id: "subscription-form" do %>
  <div class="card-fields">
    <span class="subscription-errors"></span>

    <label>
      <span>Card Number</span>
      <input value="4242 4242 4242 4242" type="text" size="20" data-stripe="number"/>
    </label>

    <label>
      <span>CVC</span>
      <input value="123" type="text" size="4" data-stripe="cvc"/>
    </label>

    <label>
      <span>Expiration</span>
      <input value="12" type="text" size="2" data-stripe="exp-month"/>
      <input value="2020" type="text" size="4" data-stripe="exp-year"/>
    </label>
  </div>
  <button type="submit">Submit Payment</button>
<% end %>

Subscriptions JavaScript

Here we will use jQuery and Stripe JS (V2) in order to generate the Stripe Token and validate the card information. If there are no errors, the form will submit to our backend API.

app/assets/javascripts/subscriptions.js

var stripeResponseHandler;

jQuery(function() {
  Stripe.setPublishableKey($("meta[name='stripe-key']").attr("content"));
  $('#subscription-form').submit(function(event) {
    var $form;
    $form = $(this);
    // Disable the submit button to prevent repeated clicks
    $form.find('button').prop('disabled', true);
    // Prevent form submittion
    Stripe.card.createToken($form, stripeResponseHandler);
    return false;
  });
});

stripeResponseHandler = function(status, response) {
  var $form, token;
  $form = $('#subscription-form');
  if (response.error) {
    $form.find('.subscription-errors').text(response.error.message);
    $form.find('button').prop('disabled', false);
  } else {
    token = response.id;
    $form.append($('<input type="hidden" name="payment_gateway_token" />').val(token));
    $form.get(0).submit();
  }
};

Application layout

We will insert the Stripe JS (V2) script tag so we can generate the payment_gateway_token, which is going to be needed to create a Stripe subscription. Basically a Stripe Token is a key that represent our credit card information.

NOTE: I'm using Rails 5.x encrypted credentials to get the stripe public key content

app/views/layouts/application.html.erb

<html>
<head>
  <title>Subscriptions</title>
  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%= javascript_include_tag 'application', 'https://js.stripe.com/v2/' %>
  <%= csrf_meta_tags %>
  <%= tag :meta, name: "stripe-key",
    content: Rails.application.credentials.stripe_public %>
</head>
<body>
  <%= yield %>
</body>
</html>

Subscriptions controller

This controller will use a service that takes care of processing the payment (we will see the service object later).

app/controllers/subscriptions_controller.rb

class SubscriptionsController < ApplicationController
  rescue_from PaymentGateway::CreateSubscriptionServiceError do |e|
    redirect_to root_path, alert: e.message
  end

  before_action :authenticate_user!
  before_action :load_plan

  def new
    @subscription = Subscription.new
  end

  def show
    @subscription = current_user.subscriptions.find(params[:id])
  end

  def create
    service = PaymentGateway::CreateSubscriptionService.new(
      user: current_user,
      plan: @plan,
      token: params[:payment_gateway_token])
    if service.run && service.success
      redirect_to plan_subscription_path(@plan,
        service.subscription),
        notice: "Your subscription has been created."
    else
      render :new
    end
  end

  private

  def load_plan
    @plan = Plan.find(params[:plan_id])
  end
end

Stipe Client

We need a wrapper between our application and the Stripe library. We are going to create a class to delegate all the Stripe methods. It's going to be worth it, trust me!

app/services/paymentgateway/stripeclient.rb

class PaymentGateway::StripeClient

  def lookup_customer(identifier: )
    handle_client_error do
      @lookup_customer ||= Stripe::Customer.retreive(identifier)
    end
  end

  def lookup_plan(identifier: )
    handle_client_error do
      @lookup_plan ||= Stripe::Plan.retreive(identifier)
    end
  end

  def lookup_event(identifier: )
    handle_client_error do
      @lookup_event ||= Stripe::Event.retreive(identifier)
    end
  end

  def create_customer!(options={})
    handle_client_error do
      Stripe::Customer.create(email: options[:email])
    end
  end

  def create_plan!(product_name, options={})
    handle_client_error do
      Stripe::Plan.create(
        id: options[:id],
        amount: options[:amount],
        currency: options[:amount] || "usd",
        interval: options[:interval] || "month",
        product: {
          name: product_name
        }
      )
    end
  end

  def create_subscription!(customer: , plan: , source: )
    handle_client_error do
      customer.subscriptions.create(
        source: source,
        plan: plan.id
      )
    end
  end

  private def handle_client_error(message=nil, &block)
    begin
      yield
    rescue Stripe::StripeError => e
      raise PaymentGateway::StripeClientError.new(e.message)
    end
  end
end

Our Client

We are going to consume the Stripe Client methods through another class. Why? First, this will help us if we switch to another payment gateway. Second, the code is going to be extremely easy to test with this design. Another reason is because handling exceptions in this way is easy since every level has its own exceptions.

app/services/payment_gateway/client.rb

class PaymentGateway::Client
  attr_accessor :external_client

  def initialize(external_client: PaymentGateway::StripeClient.new)
    @external_client = external_client
  end

  def method_missing(*args, &block)
    begin
      external_client.send(*args, &block)
    rescue => e 
      raise PaymentGateway::ClientError.new(e.message)
    end
  end
end

Grandpa

All of our payment gateway services will inherit from this class. Why? Because it defines the client which will be used in all of our payment gateway services.

app/services/payment_gateway/service.rb

class PaymentGateway::Service

  protected def client
    @client ||= PaymentGateway::Client.new
  end
end

Implementing Service: Create Subscription Service class

Testable code rocks that's why we will build a service to delegate the subscription creation. As you can see our service doesn't know anything about Stripe. It just works!

app/services/paymentgateway/createsubscription_service.rb

class PaymentGateway::CreateSubscriptionService < Service
  ERROR_MESSAGE = "There was an error while creating the subscription"
  attr_accessor :user, :plan, :token, :subscription, :success

  def initialize(user:, plan:, token:)
    @user = user
    @plan = plan
    @token = token
    @successs = false
  end

  def run
    begin
      Subscription.transaction do
        create_client_subscription
        self.subscription = create_subscription
        self.success = true
      end
    rescue PaymentGateway::CreateCustomerService, 
      PaymentGateway::CreatePlanService,
      PaymentGateway::ClientError => e
      raise PaymentGateway::CreateSubscriptionServiceError.new(
        ERROR_MESSAGE,
        exception_message: e.message)
    end
  end

  private def create_client_subscription
    client.create_subscription!(
      customer: payment_gateway_customer,
      plan: paymeny_gateway_plan,
      token: token)
  end

  private def create_subscription
    Subscription.create!(user: user,
      plan: plan,
      start_date: Time.zone.now.to_date,
      end_date: plan.end_date_from,
      status: :active)
  end

  private def payment_gateway_customer
    create_customer_service = PaymentGateway::CreateCustomerService.new(
      user: user)
    create_customer_service.run
  end

  private def paymeny_gateway_plan
    get_plan_service = PaymentGateway::GetPlanService.new(
      plan: plan)
    get_plan_service.run
  end
end

Implementing Service: Create Customer Service class

Again, we create another service to delegate the customer creation.

app/services/paymentgateway/createcustomer_service.rb

class PaymentGateway::CreateCustomerService < Service
  EXCEPTION_MESSAGE = "There was an error while creating the customer"
  attr_accessor :user

  def initialize(user: )
    @user = user
  end

  def run
    begin
      User.transaction do
        client.create_customer!(email: user.email).tap do |customer|
          user.update!(payment_gateway_customer_identifier: customer.id)
        end
      end
    rescue ActiveRecord::RecordInvalid,
      PaymentGateway::ClientError => e
      raise PaymentGateway::CreateCustomerService.new(
        EXCEPTION_MESSAGE,
        exception_message: e.message)
    end
  end
end

Implementing Service: Create Plan Service class

We'll delegate the Stripe plan creation to the CreatePlanService

app/services/createplanservice.rb

class PaymentGateway::CreatePlanService < Service
  EXCEPTION_MESSAGE = "There was an error while creating the plan"
  attr_accessor :payment_gateway_plan_identifier, :name,
    :price_cents, :interval

  def initialize(payment_gateway_plan_identifier:, name:,
      price_cents:, interval:)
    @payment_gateway_plan_identifier = payment_gateway_plan_identifier
    @name = name
    @price_cents = price_cents
    @interval = interval
  end

  def run
    begin
      Plan.transaction do
        create_client_plan
        create_plan
      end
    rescue ActiveRecord::RecordInvalid, PaymentGateway::ClientError => e
      raise PaymentGateway::CreatePlanServiceError.new(EXCEPTION_MESSAGE,
        exception_message: e.message)
    end
  end

  private def create_client_plan
    client.create_plan!(
      name,
      id: payment_gateway_plan_identifier,
      amount: price_cents,
      currency: "usd",
      interval: interval)
  end

  private def create_plan
    Plan.create!(
      payment_gateway_plan_identifier: payment_gateway_plan.id,
      name: name,
      price_cents: price_cents,
      interval: interval,
      status: :active)
  end
end

Implementing Service Error class

Let's create the ServiceError class and it's children. These will help us to handle OUR OWN application exceptions. It is a good idea to raise our own exceptions: Imagine you want to switch to another platform like Braintree... without this implementation you will have to hunt down all the places where you rescue Stripe exceptions and change them to Braintree exceptions (not so cool). This approach will simplify our lives since we will not need to worry about modifying library-specific errors all over our code. (We will talk more about this later).

app/services/service_error.rb

class PaymentGateway::ServiceError < StandardError
  attr_reader :exception_message

  def initialize(message, exception_message: )
    # Call the parent's constructor to set the message
    super(message)

    # Store the exception_message in an instance variable
    @exception_message = exception_message
  end
end

class PaymentGateway::CreateSubscriptionServiceError < PaymentGateway::ServiceError
end

class PaymentGateway::CreatePlanServiceError < PaymentGateway::ServiceError
end

class PaymentGateway::CreateCustomerServiceError < PaymentGateway::ServiceError
end

class PaymentGateway::StripeClientError < PaymentGateway::ServiceError
end

Creating plans!

lib/tasks/plans.rake

namespace :plans do
  task create: :environment do
    plans = [
        {payment_gateway_plan_identifier: "gold", name: "Gold",
         price_cents: 20_000, interval: "monthly"},
        {payment_gateway_plan_identifier: "silver", name: "Silver",
         price_cents: 10_000, interval: "monthly"}
    ]
    Plan.transaction do
      begin 
        plans.each do |plan| 
          PaymentGateway::CreatePlanService.new(**plan).run 
        end
      rescue PaymentGateway::CreatePlanServiceError => e
        puts "Error message: #{e.message}"
        puts "Exception message: #{e.exception_message}"
      end
    end
  end
end

Stripe Webhooks

We'll set up Stripe webhooks to listen for subscriptions changes; this will allow us to register/track subscriptions changes locally in our application. For example, you can send emails, create online notifications - or similar - to inform a user about subscription changes.

We'll use the StripeEvent gem which will allow us to receive Stripe events in our application.

Routes so far

config/routes.rb

Rails.application.routes.draw do
  root to: "pages#index"
  devise_for :users
  resources :plans do
    resources :subscriptions
  end
  mount StripeEvent::Engine, at: '/stripe_events'
end

Gemfile so far

source "https://rubygems.org"
ruby "2.5.1"

gem "rails", "~> 5.2.0"
gem "devise"
gem "jquery-rails"
gem "money-rails"
gem "stripe"
...

Event model

Registering events locally is a great idea. First, because requesting info from an external API is slow. Second, this will help if you want to do analytics with the data. Third, you can customise the data. For now, let's create a simple event model it will save all the event payload in a JSONB column.

rails generate model event payment_gateway_event_data:jsonb

Configure StripeEvent

Setting up this gem is pretty straightforward, we will tell StripeEvent which events are of interest. For now, we will only handle one event (invoice payment failed), but you can handle ALL of them if you want.

config/initializers/stripe.rb

Stripe.api_key             = ENV['STRIPE_SECRET_KEY']
StripeEvent.signing_secret = ENV['STRIPE_SIGNING_SECRET']

StripeEvent.configure do |events|
  events.subscribe(
    'invoice.payment_failed',
    PaymentGateway::Events::InvoicePaymentFailed.new)
end

Implementing Service: Get Event Service class

This class returns the Stripe event; we are paranoid that's why we want to verify the event from Stripe.

app/services/paymentgateway/getplan_service.rb

class PaymentGateway::GetPlanService < Service
  attr_accessor :payment_gateway_event_identifier

  def initialize(payment_gateway_event_identifier: )
    @payment_gateway_event_identifier = payment_gateway_event_identifier
  end

  def run
    begin
      get_client_event
    rescue PaymentGateway::ClientError => e
      raise CreatePlanServiceError.new("There was an error while retreiving the event", exception_message: e.message)
    end
  end

  private def get_client_event
    client.lookup_plan(identifier: payment_gateway_event_identifier)
  end
end

Handling Events: Invoice Payment Failed class

Our invoice payment failed class will handle the Stripe event, and we will build the class in such a way that is it going to create the event locally. But you can do a lot here... For instance you can send emails, broadcast an action cable channel, or anything like that.

This class creates an event locally using the webhook information AND the verified information, we are paranoid that's why we want to verify the event from Stripe.

app/services/paymentgateway/events/invoicepayment_failed.rb

class PaymentGateway::Events::InvoicePaymentFailed
  def call(payment_gateway_event)
    create_event(verified_payment_gateway_event(payment_gateway_event))
  end

  private create_event(event)
    Event.create!(JSON.parse(event.to_json))
  end

  private get_payment_gateway_event(event)
    get_plan_service = PaymentGateway::GetPlanService.new(event.id)
    get_plan_service.run
  end
end

IMPORTANT

Here is a list of important things to keep in mind while implementing webhooks:

  • Always verify that the data comes from the webhook requests
  • If you don't verify the events from Stripe, NEVER allow access to paid services in your application because "hacking" these webhook endpoints is extremely easy.

Key learnings

We have learned how to implement subscriptions with Stripe, we also learned how to design elegant services classes, finally we also learned how to implement Stripe webhooks.

Tips and advice

Make sure to let your users know that you are not storing credit card information in your systems. Automated testing is really important since you are dealing with real money, but I'm not covering that in this post since it's a large subject. Webhook testing can be done with ngrok.

Final thoughts and next steps

As you can see implementing subscriptions with Stripe is pretty simple, the documentation is extremely detailed Stripe API Reference make sure to take a look whenever you need to look example responses.

To find out how reinteractive can turn your web application vision into reality, get in touch with us through our contact form or call us on +61 2 8019 7252.