Building an Online Shop with STRIPE Integration Ridiculously Fast

Haseeb Chaudhary
8 min readJun 24, 2021
\\Photo by Mika Baumeister on Unsplash

Do you want to get a Ecomm shopping cart up and running super quick? Perhaps you just don’t want the extra costs of Shopify or other platforms? Or maybe you just want to play with payment systems? In this quick blog I’ll take you through every step to get a Online Shop up and running with a close to zero cost. Quickly.

The data flow is as follows:

Product Page — Checkout Summary — Payment Form — STRIPE — Order Page

This article describes how to code the whole of the data flow described above into Production ready environment using freely available coding frameworks, libraries and interfaces.

Setup

Getting your development environment setup isn’t something I’m going to dive into during this piece. What you should do is get the following all working in your dev environment:

Rails 6 (I used Rails 6.0.3.7)

Ruby 2.6 (I used ruby 2.6.6p146)

The rest is just rails GEMs (libraries) which I’ll show you how to install as we go along.

Fasten Your Seat Belt

The beauty of using using Ruby on Rails is we can create a new application, connect it to a database, bundle in essential JS files using Webpack, and libraries easily. The template used in this case first injects the necessary GEMs and stylesheet assets. Then the routes, app controller, devise authentication and environment variables are set. We’re going to build a carpet shop called “Delcalifa” which sells three different carpets. Begin by starting arails app as follows:

rails new \
--database postgresql \
--webpack \
-m https://raw.githubusercontent.com/haseebc/rails-templates/master/devise.rb \
delcalifa

Okay now we start the heavy lifting of working on the database schema, application routes and front end views. We need to do this for the Product Page, Stripe integration and then the Order summary. Vamos, lets begin.

Product Page

The first thing we want to do is get the product page setup.

Model

A model maintains the relationship between the objects and the database and handles validation, association, transactions, and more.

This subsystem is implemented in ActiveRecord library, which provides an interface and binding between the tables in a relational database and the Ruby program code that manipulates database records. Ruby method names are automatically generated from the field names of database tables.

rails g model Category name:string
rails g model Carpet sku:string name:string category:references photo_url:string

Then create the fileseeds.rb which resides in the db directory.

puts 'Cleaning database...'
Carpet.destroy_all
Category.destroy_all
puts 'Creating categories...'
antique = Category.create!(name: 'antique')
modern = Category.create!(name: 'modern')
puts 'Creating carpets...'
Carpet.create!(sku: 'original-carpet', name: 'Traditional carpet', category: antique, photo_url: 'http://onehdwallpaper.com/wp-content/uploads/2015/07/Teddy-Bears-HD-Images.jpg')
Carpet.create!(sku: 'modern-turkish', name: 'Jean-Michel - Le Wagon', category: modern, photo_url: 'https://pbs.twimg.com/media/B_AUcKeU4AE6ZcG.jpg:large')
Carpet.create!(sku: 'modern-iranian', name: 'Octocat - GitHub', category: modern, photo_url: 'https://cdn-ak.f.st-hatena.com/images/fotolife/s/suzumidokoro/20160413/20160413220730.jpg')
puts 'Finished!'

Populate the db by rails seeds.rb.

Controller

This is the facility within the application that directs traffic, on the one hand, querying the models for specific data, and on the other hand, organizing that data (searching, sorting, messaging it) into a form that fits the needs of a given view.

This subsystem is implemented in ActionController, which is a data broker sitting between ActiveRecord (the database interface) and ActionView (the presentation engine).

rails g controller carpets

Routes

Routes are rules written in a Ruby DSL (Domain-Specific Language). We edit the routes.rb file.

  devise_for :users
root to: 'carpets#index'
resources :carpets, only: [:index, :show]

Update the controller for carpets

# app/controllers/carpets_controller.rb
class CarpetsController < ApplicationController
skip_before_action :authenticate_user!
def index
@carpets = Carpet.all
end
def show
@carpet = Carpet.find(params[:id])
end
end

Views

carpets/index.html.erb<div class="container">
<h1>Carpets</h1>
<div class="row">
<% @carpets.each do |carpet| %>
<div class="col-6 col-md-4">
<div class="card">
<%= image_tag carpet.photo_url , class: "card-img-top" %>
<div class="card-body">
<h5 class="card-title"><%= carpet.name %></h5>
<p class="card-text"><%= carpet.category.name %></p>
<%= link_to "More details", carpet_path(carpet), class: "btn btn-primary" %>
</div>
</div>
</div>
<% end %>
</div>
</div>
carpets/show.html.erb
<div class="container py-3">
<div class="row">
<div class="col-sm-12 col-md-6">
<%= image_tag(@carpet.photo_url, width: '100%') %>
</div>
<div class="col-sm-12 col-md-6">
<h1><%= @carpet.name %></h1>
<p>Some awesome description of our amazing carpets. Cool</p>
</div>
</div>
</div>

Now we need to set the default currency, we begin by adding the necessary GEM.

# Gemfile
gem 'money-rails'
bundle
rails g money_rails:initializer

Edit the newly created ruby file, in this case we go for euros.

# config/initializers/money.rb
# https://github.com/RubyMoney/money#deprecation
Money.locale_backend = :currency
MoneyRails.configure do |config|
config.default_currency = :eur # or :gbp, :usd, etc.
# [...]
end

Run a migration

rails db:migrateEnable monetize
# app/models/carpet.rb
class Carpet < ApplicationRecord
belongs_to :category
monetize :price_cents
end

DATABASE

Don’t forget to set price in your seeds that you created earlier! Updated file below:

puts 'Cleaning database...'
Carpet.destroy_all
Category.destroy_all
puts 'Creating categories...'
antique = Category.create!(name: 'antique')
modern = Category.create!(name: 'modern')
puts 'Creating carpets...'
Carpet.create!(price_cents: 950, sku: 'original-carpet', name: 'Traditional carpet', category: antique, photo_url: 'https://cdn-ak.f.st-hatena.com/images/fotolife/s/suzumidokoro/20160413/20160413220730.jpg')
Carpet.create!(price_cents: 150, sku: 'modern-turkish', name: 'Jean-Michel - Le Wagon', category: modern, photo_url: 'https://pbs.twimg.com/media/B_AUcKeU4AE6ZcG.jpg:large')
Carpet.create!(price_cents: 1000, sku: 'modern-iranian', name: 'Octocat - GitHub', category: modern, photo_url: 'https://cdn-ak.f.st-hatena.com/images/fotolife/s/suzumidokoro/20160413/20160413220730.jpg')
puts 'Finished!'

Run the seeding and the migration

rails db:seed
rails db:migrate
bundle install

Now point to http://localhost:3000/carpets just to make sure all is cool, price should be in cents as part of the euro currency.

Okay so we now have products with an associated price. Now the fun starts.

Inspiring tiles at the Alhambra, nature is in the numbers.Photo by Maria Bobrova on Unsplash

STRIPE Integration

Make sure you setup a new Stripe account and then add the Stripe GEM.

# Gemfile
gem 'stripe'
bundle install

Next configure the initialiser ensuring you add the API keys to the .env file. Be super sure not to upload the keys to Github by updating .gitignore.

#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]

Make an Order

Update the model.

rails g model Order state:string carpet_sku:string amount:monetize checkout_session_id:string user:references carpet:references

Ensure you edit the migration to remove the currency.

t.monetize :amount, currency: { present: false }

The migration file should look like this:

class CreateOrders < ActiveRecord::Migration[6.0]
def change
create_table :orders do |t|
t.string :state
t.string :carpet_sku
t.string :checkout_session_id
t.references :user, null: false, foreign_key: true
t.references :carpet, null: false, foreign_key: true
t.monetize :amount, currency: { present: false }
t.timestamps
end
end
end

Then after running the migrationdb:migrate enable montize in order.rb.

# app/models/order.rb
monetize :amount_cents

Controller

rails g controller orders

Routes

# config/routes.rb
resources :orders, only: [:show, :create]

Views

Here’s we’re going to edit the carpet page and add a form to create an order.

# app/views/carpets/show.html.erb
<%= form_tag orders_path do %>
<%= hidden_field_tag 'teddy_id', @teddy.id %>
<%= submit_tag 'Purchase', class: 'btn btn-primary' %>
<% end %>

Controllers

# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def create
carpet = Carpet.find(params[:carpet_id])
order = Order.create!(carpet: carpet, carpet_sku: carpet.sku, amount: carpet.price, state: 'pending', user: current_user)

session = Stripe::Checkout::Session.create(
payment_method_types: ['card'],
line_items: [{
name: carpet.sku,
images: [carpet.photo_url],
amount: carpet.price_cents,
currency: 'eur',
quantity: 1
}],
success_url: order_url(order),
cancel_url: order_url(order)
)

order.update(checkout_session_id: session.id)
redirect_to new_order_payment_path(order)
end
end

Accepting Payments

Here we make a routes and controller modification. Then change the view for the payment form.

Routes

# config/routes.rb
resources :orders, only: [:show, :create] do
resources :payments, only: :new
end

Controller

Edit Payment controller

class PaymentsController < ApplicationController
def new
@order = current_user.orders.where(state: 'pending').find(params[:order_id])
end
end

Views

<!-- app/views/payments/new.html.erb -->
<div class="container">
<div class="row py-4">
<div class="col-md-3"><%= image_tag @order.carpet.photo_url, width: '100%' %></div>
<div class="col">
<h1>Checkout summary</h1>
<p>Purchase of <strong><%= @order.carpet.name %></strong> for <strong><%= humanized_money_with_symbol @order.amount %></strong></p>
<button id="pay" class="btn btn-primary">Pay</button>
<!-- # Commented for the lecture, remove comments. -->
<script src="https://js.stripe.com/v3/"></script>
<script>
const paymentButton = document.getElementById('pay');
paymentButton.addEventListener('click', () => {
const stripe = Stripe('<%= ENV['STRIPE_PUBLISHABLE_KEY'] %>');
stripe.redirectToCheckout({
sessionId: '<%= @order.checkout_session_id %>'
});
});
</script>
</div>
</div>
</div>

Controller

Edit the Orders controller

def show
@order = current_user.orders.find(params[:id])
end

Views

<div class="container">
<div class="row py-4">
<div class="col-md-3"><%= image_tag @order.carpet.photo_url, width: '100%' %></div>
<div class="col">
<h1>Order #<%= @order.id %> <small><span class="badge badge-primary"><%= @order.state %></span></small></h1>
<ul class="list-unstyled">
<li><strong>ISSUED ON</strong> <%= @order.created_at.to_date %></li>
<li><strong>CARPET</strong> <%= @order.carpet.name %> [<%= @order.carpet_sku %>]</li>
<li><strong>TOTAL</strong> <%= humanized_money_with_symbol @order.amount %></li>
</li>
</ul>
<p>Estimated delivery in <strong>2 business days</strong></p>
</div>
</div>
</div>

Updating the Order

Get Github setup

Create new repository called delcalifa_stripe.

git remote add origin git@github.com:haseebc/delcalifa_stripe.git
git push -u origin master

Production Hosting

In this case we utilise heroku for the hosting environment. Set yourself a fee account and then get going with creating the app space as follows:

heroku create delcalifanew --region eu
heroku git:remote -a delcalifanew

So how do we update the order?
Right now, the order is still pending 🤔
Lets use Stripe webhooks!

STRIPE Webhooks

Begin by adding the associated GEM file.

# Gemfile
gem 'stripe_event'

Make a route change

# config/routes.rb
mount StripeEvent::Engine, at: '/stripe-webhooks'

Webhook configuration

Go on your Stripe dashboard to create a webhook
https://dashboard.stripe.com/test/webhooks

And add your webhook

URL endpoint: <HOST URL>/stripe-webhooks

Go to the test data part of the Stripe dashboard and add this webhook

https://delcalifanew.herokuapp.com/stripe-webhooks

Use the previous Stripe publishable key and secret key that we obtained.

Webhook signing secret is specific to the endpoint.

What you will have now is two sets of important API keys. One for Test and One for Prod. Its super important to understand that Test here is all the API keys you will add to Heroku when you want to run tests. These API keys are also stored and available in the Stripe dashboard.

For example for Test

STRIPE_PUBLISHABLE_KEY
pk_test_something
STRIPE_SECRET_KEY
sk_test_something
Webhook signing secret
whsec_something

For Production

STRIPE_PUBLISHABLE_KEY
pk_live_something
STRIPE_SECRET_KEY
sk_live_something
Webhook signing secret
whsec_something

Test locally first

  1. Install ngrok
  2. /ngrok http 3000 — this will give a hook into https for rails on 3000
  3. Copy the url into Stripe webhooks
    for example https://8d57fb16f8ea.ngrok.io/stripe-webhooks
    Now run a test but dont try and run checkout session completed. Run charge succeeded or something.
    Ensure in environments development this line is there
    config.hosts.clear
  4. You’ll get a 200 in the ngrok logs

Test in Heroku

  1. Make sure webhook for test and prod is setup
    For example for test data https://delcalifanew.herokuapp.com/stripe-webhooks
    And for Pro data.
  2. In Heroku you will use test or prod webooks and STRIPE keys. Depends on what you want. I use test all the time as my shop is not live. Going live is just a cash of using the Prod API keys.

Credit card details to test with are here for Stripe:

# Standard credit card
4242 4242 4242 4242

# Credit card with 3D Secure 2
4000 0000 0000 3220

There are a lot more Stripe card details here.

Useful Links

Stripe have done a really good job on their documentation. Some links here.

https://stripe.com/docs/legacy-checkout/rails
https://stripe.com/docs/payments/checkout

Below is some further useful info on setting up a rails cart without the payment system integration.

https://github.com/howardmann/Tutorials/blob/master/Rails_Shopping_Cart.md

Security

Securing the Online Shop is relatively straightforward. You want to think about all the ways data moves between interfaces and then apply security to this (input validation, access and session management), I’ll aim to write this up soon.

However, when working with API keys make sure you make your Github repository private, with Heroku apply secondary authentication to your access and for Strip dashboard access also use secondary authentication. Mishandling the API keys can lead to lots of bad stuff quickly!

If you have any questions please feel free to contact me. When I’m not riding or traveling I build stuff at El Born Lab.

Photo by Victoriano Izquierdo on Unsplash

--

--