- Create a basic Rails application
- Do the simplest Stripe integration
- Learn how to deploy to Heroku
In this chapter we're going to create a simple Rails application so we have something to work with for later chapters. All of the rest of the examples in the guide will be based on this app.
Our app will sell downloadable products. Users will be able to create products and customers will be able to buy them, and we'll keep track of sales so we can do reporting later. Customers will be able to come back and download their purchases multiple times. We'll need three models:
Product
, representing a product that we're going to be selling.User
, for logging in and managing productsSale
,to represent each individual customer purchase
Let's create an initial application:
$ gem install rails
$ rails new sales --database postgresql
$ cd sales
$ createuser -s sales
$ rake db:create
$ rake db:migrate
$ rake test
(FYI the last three Rake commands won't output anything unless there's an error.)
I'm going to use PostgreSQL for the example app because that's what I know best,
it's what Heroku provides for free, and it's what I suggest to everyone who asks. If
you're using a Mac and don't have PostgreSQL installed, this is an excellent
tutorial, or you can install use Postgres.app. If you want to use a different
database, feel free to substitute. Any ActiveRecord
-compatible database will do
fine.
We're going to want to be able to authenticate users who can add and manage
products and view sales. The example is going to use a gem named Devise which
handles everything user-related out of the box. Add it to your Gemfile
:
gem ('devise', '~> 3.4.1'_)
then run bundler and set up Devise:
$ bundle install
$ rails generate devise:install)
At this point you have to do some manual configuration. Add this to
config/environments/development.rb
:
config.action_mailer.default_url_options = {
:host => 'localhost:3000'
}
and this in app/views/layouts/application.html.erb
right after the body
tag:
<% flash.each do |type, msg| %>
<%= content_tag :p, msg, class: type %>
<% end %>
Now, let's create a User model for Devise to work with:
$ rails generate devise User
$ rake db:migrate
Open up app/controllers/application_controller.rb
and add this line which will secure everything by default:
before_action :authenticate_user!
You'll need to create a user so you can actually log in to the site. Fire up rails console
and type:
User.create!(
email: '[email protected]',
password: 'password', # has to be at least 8 characters
password_confirmation: 'password'
)
Our sales site needs something to sell, so let's create a product model:
$ rails g scaffold Product \
name:string \
permalink:string \
description:text \
price:integer \
user:references
$ rake db:migrate
name
and description
will be displayed to the customer, permalink
will be used later. Open up app/models/product.rb
and change it to look like this:
class Product < ActiveRecord::Base
has_attached_file :file
belongs_to :user
end
Note the has_attached_file
. We're using Paperclip to attach the downloadable files to the product record. Let's add it to Gemfile
:
gem 'paperclip', '~> 4.2.1'
And bundle install
again to get Paperclip installed.
Now we need to generate the migration so paperclip has a place to keep the file metadata:
$ rails generate paperclip product file
We should add an upload button to the Product edit form as well. In
app/views/products/_form.html.erb
inside the form_for
below the other fields:
<div class="field">
<%= f.label :file %><br />
<%= f.file_field :file %>
</div>
While you're in the _form
partial, remove the user_id
field. We'll populate it in the
controller, ProductsController#create
:
respond_to :html, :json
def create
@product = Product.new(product_params)
@product.user = current_user
@product.save
respond_with(@product)
end
We should also re-define product_params
in ProductsController
toward the bottom.
Note that we include the file
attribute for Paperclip:
private
def product_params
params.require(:product).permit(:description, :name, :permalink, :price, :file)
end
We don't allow the user_id
because we're setting it explicitly to current_user
. This
is a security precaution so that any given user can only create products for
themselves, instead of for other users.
Our app needs a way to track product sales. Let's make a Sale model too.
$ rails g scaffold Sale \
email:string \
guid:string \
product:references \
stripe_id:string
$ rake db:migrate
Open up app/models/sale.rb
and make it look like this:
class Sale < ActiveRecord::Base
belongs_to :product
before_save :populate_guid
validates_uniqueness_of :guid
private
def populate_guid
if new_record?
while !valid? || self.guid.nil?
self.guid = SecureRandom.random_number(1_000_000_000).to_s(36)
end
end
end
end
We're using a GUID here so that when we eventually allow the user to look at their
transaction they won't see the id
, which means they won't be able to guess the
next ID in the sequence and potentially see someone else's transaction. This isn't
an official UUID since those tend to be long and awkward. Instead, we pick a
number between 0 and one billion, turn it into a string by encoding it with base 36
(lowercase letters a-z and numbers 0-9). Then we test to make sure the record is
valid. The loop will continue until there's a unique GUID value because of the
validates_uniqueness_of_
on _:guid
.
We should also add the relationship to Product
:
class Product < ActiveRecord::Base
belongs_to :user
has_many :sales
validates_numericality_of :price,
greater_than: 49,
message: "must be at least 50 cents"
has_attached_file :file
validates_attachment_content_type :file, :content_type => [
"image/jpg",
"image/jpeg",
"image/png",
"image/gif",
"application/pdf",
"application/zip"
]
end
Stripe does not allow charges less than $0.50, so we add a validation to make sure a product doesn't end up like that.
We're also setting up the paperclip integration on Product
. The first line,
has_attached_file :file
, tells Paperclip to add the appropriate access methods. The
second section, validates_attachment_content_type
, ensures that only files of the
specified content types get uploaded. This list includes several image types as well
as PDFs and Zip files. If you're going to be selling something else, make sure to add
the appropriate MIME types here.
At this point, you should be able to fire up rails server
and create a product or two
by going to http://localhost:3000/products.
Heroku is the fastest way to get a Rails app deployed into a production
environment so that's what we're going to use throughout the guide. If you already
have a deployment system for your application by all means use that. First,
download and install the Heroku Toolbelt for your platform. Make sure you heroku login
to set your credentials.
We'll need to add one more thing, since Rails' asset pipeline doesn't play well with
Heroku. Add this to Gemfile
and run bundle install
one more time:
gem 'rails_12factor', group: :production
Next, create an application and deploy the example code to it:
$ git init
$ git add .
$ git commit -m 'Initial commit'
$ heroku create
$ git push heroku master
$ heroku run rake db:migrate
$ heroku run console # create a user
$ heroku restart web
$ heroku open
We'll need to set a few more config options to make our site usable on Heroku.
First, we need to set up an outgoing email server and configure ActionMailer
to use
it. Let's add the SparkPost addon:
$ heroku addons:create sparkpost:free
Now configure it in config/environments/production.rb
:
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV['SPARKPOST_SMTP_HOST'],
port: ENV['SPARKPOST_SMTP_PORT'],
user_name: ENV['SPARKPOST_SMTP_USERNAME'],
password: ENV['SPARKPOST_SMTP_PASSWORD'],
domain: 'heroku.com',
authentication: :plain
}
config.action_mailer.default_url_options = {
:host => 'your-app.herokuapp.com'
}
SparkPost is the same engine that powers email for huge companies like Twitter
and SalesForce. It's reliable, powerful, and cost effective. They give you 100,000
emails per month for free to get started. I use it for all of my applications. Note
also that we configure the default_url_options
here again for ActionMailer. This is
what Devise uses to generate links inside emails, so it's pretty important to get it
right.
We also need to set up Paperclip to save uploaded files to S3 instead of the local file system on production. On Heroku your processes live inside what they call a dyno which is just a lightweight Linux virtual machine with your application code inside. Each dyno has an ephemeral file system which gets erased at least once every 24 hours,thus the need to push uploads somewhere else. Paperclip makes this pretty painless. You'll need to add another gem to your Gemfile:
gem 'aws-sdk', '< 2.0'
and then configure Paperclip to use it in config/environments/production.rb
:
config.paperclip_defaults = {
storage: :s3,
s3_credentials: {
bucket: ENV['AWS_BUCKET'],
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
}
}
Sign up on Amazon's site to get AWS credentials if you don't already have them.
Just set those config variables with Heroku, bundle install, and then commit and push up to Heroku.
You should see a login prompt from Devise. Go ahead and login and create a few products. We'll get to buying and downloading in the next section.
Now we're going to do a whirlwind Stripe integration, loosely based on Stripe's own Rails Checkout Guide.
Remember that this application is going to be selling digital downloads, so we're going to have three actions:
_buy_
- where we create a Sale record and actually charge the customer_pickup_
- where the customer can download their product_download_
which will actually send the file to the customer
In addition, we're going to leverage Stripe's excellent management interface which will show us all of our sales as they come in.
First, add the Stripe gemsource to the very top of your Gemfile:
source 'https://code.stripe.com'
Then, add the actual gem:
gem 'stripe', '~> 1.26.0'
And then run bundle install
.
We'll also need to set up the Stripe keys. In config/initializers/stripe.rb
:
Rails.configuration.stripe = {
publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
secret_key: ENV['STRIPE_SECRET_KEY'],
}
Stripe.api_key = \
Rails.configuration.stripe[:secret_key]
Note that we're getting the keys from the environment. This is for two reasons: first, because it lets us easily have different keys for testing and for production; second, and more importantly, it means we don't have to hard-code any potentially dangerous security credentials. Putting the keys directly in your code means that anyone with access to your code base can make Stripe transactions with your account.
You can get your publishable and secret key from your Stripe account settings in the Dashboard.
Next, let's create a new controller named Transactions
where our Stripe-related
logic will live:
In app/controllers/transactions_controller.rb
:
class TransactionsController < ApplicationController
skip_before_action :authenticate_user!,
only: [:new, :create]
def new
@product = Product.find_by!(
permalink: params[:permalink]
)
end
def pickup
@sale = Sale.find_by!(guid: params[:guid])
@product = @sale.product
end
def create
product = Product.find_by!(
permalink: params[:permalink]
)
token = params[:stripeToken]
begin
charge = Stripe::Charge.create(
amount: product.price,
currency: "usd",
source: token,
description: params[:stripeEmail]
)
@sale = product.sales.create!(
email: params[:stripeEmail],
stripe_id: charge.id
)
redirect_to pickup_url(guid: @sale.guid)
rescue Stripe::CardError => e
# The card has been declined or
# some other error has occurred
@error = e
render :new
end
end
def download
@sale = Sale.find_by!(guid: params[:guid])
resp = HTTParty.get(@sale.product.file.url)
filename = @sale.product.file.url
send_data resp.body,
:filename => File.basename(filename),
:content_type => resp.headers['Content-Type']
end
end
#new
is just a placeholder for rendering the corresponding view. The real action
happens in #create
where we look up the product and actually charge the
customer. Note that we hard-code usd
as the currency. If you have a Stripe account
in a different country you'll want to provide your country's currency code here,
which you can find at the top of your Stripe dashboard. For example, if you're in
the Eurozone you'll see "Total Sales (EUR)".
In the last chapter we included a permalink
attribute in Product
and we use that
here to look up the product, mainly because it'll let us generate nicer-looking
URLs. If there's an error we display the #new
action again. If there's not we redirect
to a route named pickup
. Inside the view for #pickup
we include link to /download
which sends the data to the user from S3.
We get the data from S3 using a gem named HTTParty
. Let's add it to the Gemfile:
gem 'httparty'
The routes for transactions are pretty simple. Add this to config/routes.rb
:
get '/buy/:permalink', to: 'transactions#new', as: :show_buy
post '/buy/:permalink', to: 'transactions#create', as: :buy
get '/pickup/:guid', to: 'transactions#pickup', as: :pickup
get '/download/:guid', to: 'transactions#download', as: :download
RESTful URLs are great if you're building a reusable API, but for this example we're writing a pretty simple website and the customer-facing URLs should look good. If you want to use resources, feel free to adjust the examples.
Time to set up the views. Put this in app/views/transactions/new.html.erb
:
<h1><%= @product.name %></h1>
<%= @product.description.html_safe %>
<% if @error %>
<%= @error %>
<% end %>
<p>Price: <%= formatted_price(@product.price) %></p>
<%= form_tag buy_path(permalink: @product.permalink) do %>
<script src="https://checkout.stripe.com/v2/checkout.js"
class="stripe-button"
data-key="<%= Rails.configuration.stripe[:publishable_key] %>"
data-description="<%= @product.name %>"
data-amount="<%= @product.price %>"></script>
<% end %>
Drop the definition for formatted_price
into app/helpers/application_helper.rb
:
def formatted_price(amount)
sprintf("$%0.2f", amount / 100.0)
end
This is a very simple example of a product purchase page with the product's name,
description, and a Stripe button using checkout.js
. Checkout puts a simple button
on your page that pops up a small overlay onto your page where the user puts in
their credit card information. Stripe automatically processes the card information
into a single use token while handling errors for you. When all of that is done
checkout.js
will submit the surrounding form to your server, taking care to strip
out sensitive information. It's a convenient way to collect card information if you
don't want to go to the trouble of making your own custom form, which we'll talk
about in a later chapter.
Notice that we just drop the description in as html which makes it a risk for cross-
site-scripting attacks. Make sure you trust the users you allow to create products.
We're rendering the new
view for the #create
action, too, so if there's an error
we'll display it above the checkout button.
The view for #pickup
is even simpler, since it basically just has to display the
product's download link. In app/views/transactions/pickup.html.erb
:
<h1>Download <%= @product.name %></h1>
<p>Thanks for buying "<%= @product.name %>". You can download your purchase by clicking the link below.</p>
<p><%= link_to "Download", download_url(guid: @sale.guid) %></p>
Testing
Testing is vitally important to any modern web application, doubly so for applications involving payments. Tests are one of the best ways to make sure your app works the way you think it does.
Manually testing your application is a good first step. Stripe provides test mode keys that you can find in your account settings. By using the test mode keys you can run transactions through Stripe with testing credit card numbers and hit not only the happy case, but also a variety of failure cases. Stripe provides a variety of credit card numbers that trigger different failure modes. Here's a small selection:
4242 4242 4242 4242
: always succeeds4000 0000 0000 0010
: address failures4000 0000 0000 0101
: cvs check failure4000 0000 0000 0002
: card will always be declined There are a bunch more failure modes you can check but those are the big ones. Make sure to manually run your test through at least these failure cases. You'll catch bugs you wouldn't think to test for and you'll actually be interacting with Stripe's API, which you won't be in your automated tests.
Manual testing is all well and good but you should also write repeatable unit and functional tests that you can run as part of your deploy process. This can get a little tricky, though, because you don't really want to be hitting Stripe's API servers with your test requests. They'll be slower and you'll pollute your testing environment with junk data.
Instead, let's use mocks and factories. In Gemfile
:
group :development do
gem 'stripe-ruby-mock'
gem 'database_cleaner'
end
StripeMock provides mocks for the entire Stripe API so your tests don't have to actually hit Stripe's servers. Database Cleaner cleans out the database between test runs.
Let's set all of this up. In test/test_helper.rb
:
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require 'database_cleaner'
require 'stripe_mock'
class ActiveSupport::TestCase
setup do
DatabaseCleaner.start
StripeMock.start
end
teardown do
DatabaseCleaner.clean
StripeMock.stop
end
end
require 'mocha/setup'
Note that Mocha must be required as the very last thing in test_helper
.
Let's write a test for TransactionsController
. In
test/functional/transactions_controller_test.rb
:
class TransactionsControllerTest < ActionController::TestCase
test "should post create" do
product = Product.create(
permalink: 'test_product',
price: 100
)
email = '[email protected]'
token = 'tok_test'
post :create, email: email, stripeToken: token
assert_not_nil assigns(:sale)
assert_not_nil assigns(:sale).stripe_id
assert_equal product.id, assigns(:sale).product_id
assert_equal email, assigns(:sale).email
end
end
This is a straight forward controller test. First, we create a Product
instance, then
we POST
at the :create
action, which will create an instance of Sale
, setting the
appropriate attributes. The important part here is that stripe_id
is populated,
which means Stripe::Mock
is doing it's job by mocking out all of the Stripe API
calls.
Add all the new files to git and commit, then run:
$ heroku config:add \
STRIPE_PUBLISHABLE_KEY=pk_test_publishable_key \
STRIPE_SECRET_KEY=sk_test_secret_key
$ git push heroku master
You should be able to navigate to https://your-app.herokuapp.com/buy/some_permalink
and click the buy button to buy and download a product.
In this chapter we built (almost) the simplest Stripe integration possible. In the next chapter we're going to take a detour and talk about PCI and what you have to do to be compliant and secure while using Stripe and Rails.
Uh oh!
There was an error while loading. Please reload this page.