Skip to content

Instantly share code, notes, and snippets.

@rexmortus
Created May 28, 2019 00:18
Show Gist options
  • Save rexmortus/71ef3e70c86c3dd561a7f602f46fe9c2 to your computer and use it in GitHub Desktop.
Save rexmortus/71ef3e70c86c3dd561a7f602f46fe9c2 to your computer and use it in GitHub Desktop.
Notes on 'Payment & Stripe'

Payment & Stripe

0.0 Pre-requisites

0.1 Check which version of Ruby is installed:

ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) 

Better yet, use which because it gives us the full path of our Ruby interpreter (and indicates whether we’re using rvm):

which ruby
~/.rvm/rubies/ruby-2.6.3/bin/ruby

0.2 What version of Rails is installed?

rails -v
Rails 5.2.3

Or:

which rails
~/.rvm/gems/ruby-2.6.3/bin/rails

0.3 Is Postgres running?

If you have the Postgres OS X app installed, simply check the menu-bar application to check.

There are multiple ways to check the status of postgres from the command line, including pg-ctl but we won’t cover that now.

0.4 Is yarn installed?

Because we’re using the webpacker gem, we’ll need yarn (the Javascript dependency manager).

brew install yarn

Then install the Javascript dependencies:

yarn install

Due to reasons, you may also need to manually install some of the Javascript dependencies:

yarn add bootstrap jquery popper.js

1. Review the payment flow diagram

1.1 What does the product page need to do?

  1. Load an instance of Teddy from the database 2. Render a template showing:
    • Teddy image
    • Teddy description
    • an action (i.e. button that POSTs form-data, creating a new Order)

1.2 What does order summary page need to do?

  1. Load the newly-created instance of Order 2. Render a template showing:
    • Teddy image
    • Teddy description
    • Teddy price
    • show the computed price information (tax, shipping, total)
    • provide an action (i.e. button that POSTs form-data to a payments controller which creates a new Stripe payment, attaches the result to corresponding Order, and then redirects to the order, showing completion)

2. Create application

Use rails new to create a new Ruby on Rails application.

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

Provided you’ve setup postgres correctly (and it’s running), rails new will automatically create a development and test database for you (default environments that every new default Rails app ships with). rails new leaves the production environment configuration blank for you to complete later.

Note that we’re also using a devise template:

-m, [--template=TEMPLATE]                                # Path to some application template (can be a filesystem path or URL)

After rails new has done its work, let’s check that our new application works properly.

rails c
Running via Spring preloader in process 60169
Loading development environment (Rails 5.2.3)
[1] pry(main)>

Note that while rails c will tell us if the core rails app is configured correctly for the development environment (i.e. mostly if it’s correctly connected to the database) it will tell us nothing about whether webpacker is working correctly, which is only initialised when we boot the development server via. rails s:

rails s
=> Booting Puma
=> Rails 5.2.3 application starting in development 
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.1 (ruby 2.6.3-p62), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3000

3. Generating models

3.1 Generating top-level categories for our products

rails g model Category name:string

name we’ll have categories like kids and geek

3.2 Generating the Teddy model

rails g model Teddy sku:string name:string category:references photo_url:string

sku means stock-keeping unit and is the unique, canonical identifier for each product.

name is the customer-facing title of the product, (i.e. what actually appears on-screen for the user as they browse the inventory)

category is a reference to the parent product category (e.g. Octocat belongs to the geek category)

Finally, migrate the database:

rake db:migrate
== 20190527004750 CreateCategories: migrating =================================
-- create_table(:categories)
   -> 0.0101s
== 20190527004750 CreateCategories: migrated (0.0103s) ========================

== 20190527005542 CreateTeddies: migrating ====================================
-- create_table(:teddies)
   -> 0.0183s
== 20190527005542 CreateTeddies: migrated (0.0185s) ===========================

Now, let’s check that our Category and Teddy models work properly within the rails console by interactively creating a new Category:

rails c
[1] pry(main)> newCategory = Category.new
=> #<Category:0x00007fd60a29dcf8 id: nil, name: nil, created_at: nil, updated_at: nil>

Now, this model exists in memory, but it hasn’t been saved to the postgres database yet:

pry(main)> newCategory.save
   (0.3ms)  BEGIN
  Category Create (3.2ms)  INSERT INTO "categories" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id"  [["created_at", "2019-05-27 01:09:49.053303"], ["updated_at", "2019-05-27 01:09:49.053303"]]
   (0.7ms)  COMMIT
=> true
pry(main)> newCategory
=> #<Category:0x00007fd60a29dcf8
 id: 1,
 name: nil,
 created_at: Mon, 27 May 2019 01:09:49 UTC +00:00,
 updated_at: Mon, 27 May 2019 01:09:49 UTC +00:00>

If the return value is => true our model has been saved to the database.

Lets make sure by loading our models straight from the database:

pry(main)> Category.all
  Category Load (0.6ms)  SELECT "categories".* FROM "categories"
=> [#<Category:0x00007fd6099f8378
  id: 1,
  name: nil,
  created_at: Mon, 27 May 2019 01:16:01 UTC +00:00,
  updated_at: Mon, 27 May 2019 01:16:01 UTC +00:00>]

Ok, but what’s the problem with this? The new category has no title because the model doesn’t have an appropriate NOT NULL constraint!

First, let’s clean up our newly created category:

newCategory.delete
Category Load (0.4ms)  SELECT  "categories".* FROM "categories" WHERE "categories"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
  Category Destroy (5.8ms)  DELETE FROM "categories" WHERE "categories"."id" = $1  [["id", 2]]
=> #<Category:0x00007fd60d2b48d8
 id: 2,
 name: nil,
 created_at: Mon, 27 May 2019 01:16:01 UTC +00:00,
 updated_at: Mon, 27 May 2019 01:16:01 UTC +00:00>

Break out of the rails console:

pry(main)> exit

Lets generate a new migration:

rails generate migration AddNotNullToCategoryName

Open the new migration and use change_column_null to add a constraint:

class AddNotNullToCategoryName < ActiveRecord::Migration[5.2]
  def change
    change_column_null :categories, :name, false
  end
end

Save the migration, and run the new migration:

rake db:migrate
== 20190527012308 AddNotNullToCategoryName: migrating =========================
-- change_column_null(:categories, :name, false)
   -> 0.0029s
== 20190527012308 AddNotNullToCategoryName: migrated (0.0032s) ================

From within rails console create a new Category and try saving it:

pry(main)> kidsCategory = Category.new
=> #<Category:0x00007fd60d1269f8 id: nil, name: nil, created_at: nil, updated_at: nil>

[2] pry(main)> kidsCategory.save
   (0.3ms)  BEGIN
  Category Create (2.1ms)  INSERT INTO "categories" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id"  [["created_at", "2019-05-27 01:26:45.199308"], ["updated_at", "2019-05-27 01:26:45.199308"]]
   (0.4ms)  ROLLBACK
ActiveRecord::NotNullViolation: PG::NotNullViolation: ERROR:  null value in column "name" violates not-null constraint

That’s better! Let’s give our Category a valid name and save it:

pry(main)> kidsCategory.name = "kids"
=> "kids"
[4] pry(main)> newCategory.save
   (0.2ms)  BEGIN
  Category Create (0.6ms)  INSERT INTO "categories" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "kids"], ["created_at", "2019-05-27 01:26:45.199308"], ["updated_at", "2019-05-27 01:26:45.199308"]]
   (1.4ms)  COMMIT
=> true

Now, let’s create a Teddy with an association to the “kids” Category:

pry(main)> kidsTeddy = Teddy.new
=> #<Teddy:0x00007fd60dad1728 id: nil, sku: nil, name: nil, category_id: nil, photo_url: nil, created_at: nil, updated_at: nil>

pry(main)> kidsTeddy.category = kidsCategory
=> #<Category:0x00007fd60d1deda0
 id: 5,
 name: "kids",
 created_at: Mon, 27 May 2019 01:31:20 UTC +00:00,
 updated_at: Mon, 27 May 2019 01:31:20 UTC +00:00>

pry(main)> kidsTeddy.name = 'Professor Crumbling'
=> "Professor Crumbling"

pry(main)> kidsTeddy.sku = 'professor-crumbling'
=> "professor-crumbling"

pry(main)> kidsTeddy.save
   (0.3ms)  BEGIN
  Teddy Create (1.6ms)  INSERT INTO "teddies" ("sku", "name", "category_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["sku", "professor-crumbling"], ["name", "Professor Crumbling"], ["category_id", 5], ["created_at", "2019-05-27 01:35:18.020719"], ["updated_at", "2019-05-27 01:35:18.020719"]]
   (0.6ms)  COMMIT
=> true

Now, when we inspect the newly created Teddy we should see all the fields have been saved:

pry(main)> kidsTeddy
=> #<Teddy:0x00007fd60dad1728
 id: 1,
 sku: "professor-crumbling",
 name: "Professor Crumbling",
 category_id: 5,
 photo_url: nil,
 created_at: Mon, 27 May 2019 01:35:18 UTC +00:00,
 updated_at: Mon, 27 May 2019 01:35:18 UTC +00:00>

Working with models in pry is a crucial skill, because testing the functionality of this layer directly (without having to use controllers and their various actions) will save you heaps of time, both in terms of designing and debugging.

However, what we really want is default test data so that when someone pulls down our application they can work with it immediately without having to go through the extra step of creating their own, or fetching or loading it from somewhere else. We’ll use seeds for that.

First, let’s clean up our database. Break our of rails c and run:

rake db:reset

Open db/seeds and enter the following:

puts 'Cleaning database...'
Teddy.destroy_all
Category.destroy_all

puts 'Creating categories...'
geek = Category.create!(name: 'geek')
kids = Category.create!(name: 'kids')

puts 'Creating teddies...'
Teddy.create!(sku: 'original-teddy-bear', name: 'Teddy bear', category: kids, photo_url: 'http://onehdwallpaper.com/wp-content/uploads/2015/07/Teddy-Bears-HD-Images.jpg')

Teddy.create!(sku: 'jean-mimi', name: 'Jean-Michel - Le Wagon', category: geek, photo_url: 'https://pbs.twimg.com/media/B_AUcKeU4AE6ZcG.jpg:large')
Teddy.create!(sku: 'octocat',   name: 'Octocat -  GitHub',      category: geek, photo_url: 'https://cdn-ak.f.st-hatena.com/images/fotolife/s/suzumidokoro/20160413/20160413220730.jpg')
puts 'Finished!'

Now run rails db:seed to create our test data:

rails db:seed
Cleaning database...
Creating categories...
Creating teddies...
Finished!

Now, if you re-enter rails console and run Category.all you will see our newly created test data:

rails c
Running via Spring preloader in process 68408
Loading development environment (Rails 5.2.3)
pry(main)> Category.all
  Category Load (1.2ms)  SELECT "categories".* FROM "categories"
=> [#<Category:0x00007fd6090e4200
  id: 1,
  name: "geek",
  created_at: Mon, 27 May 2019 01:47:51 UTC +00:00,
  updated_at: Mon, 27 May 2019 01:47:51 UTC +00:00>,
 #<Category:0x00007fd60d98ef50
  id: 2,
  name: "kids",
  created_at: Mon, 27 May 2019 01:47:51 UTC +00:00,
  updated_at: Mon, 27 May 2019 01:47:51 UTC +00:00>]

Fantastic! Our basic model layer is up-and-running.

4. Listing our Teddies

Now that we’ve created our model layer, we’re ready to start rigging up the screens that make up our simple ecommerce app. We’ll start with the simplest part: listing all our teddies.

Let’s use rails generate to create a new controller.

rails g controller teddies
Running via Spring preloader in process 69178
      create  app/controllers/teddies_controller.rb
      invoke  erb
      create    app/views/teddies
      invoke  test_unit
      create    test/controllers/teddies_controller_test.rb

This creates:

  1. teddies_controller.rb which implements TeddiesController: the handler for GET /teddies and GET /teddies/<id>.
  2. An empty folder at app/views/teddies where the template files go
  3. teddies_controller_test.rb which implements TeddiesControllerTest: which implements integration tests for TeddiesConstroller

We’re going to skip the integration tests for now, however, if we were doing this the proper way, we would start with the integration tests. So-called “Test-Driven Development” (TDD) isn’t just sound practice, it’s also a way to sketch out, at a high-level, how everything in your application should work. It’s quicker task than implementation. and helps you stay on track because the tests serve as a sort-of development checklist, presenting the specific features to develop, and the most sensible order to implement them in.

Ok, so we have our TeddiesController which will handle GET /teddies and GET /teddies/<id> , but it won’t work until we alter our routing configuration. Open config/routes.rb:

Rails.application.routes.draw do
  devise_for :users
  root 'teddies#index'
  resources :teddies, only: [:index, :show]
end

Replace:

root to: 'pages#home'

with:

  root 'teddies#index'

Here, we are using the root keyword to plug GET / into TeddiesContoller#index .

Next, add:

 resources :teddies, only: [:index, :show]

This plugs:

  • GET /teddies into TeddiesController#index
  • GET /teddies/:id into TeddiesController#show

Now that we’ve configured our router, lets implement the route handlers. Open teddies_controller.rb:

# app/controllers/teddies_controller.rb
class TeddiesController < ApplicationController
end

Right now, when our router passes GET /teddies & GET /teddies/:id to TeddiesController… ain’t nothing gonna happen:

GET /teddies
AbstractController::ActionNotFound (The action 'index' could not be found for TeddiesController):

We told our router to pass those requests to #index and #show, so lets implement those:

# app/controllers/teddies_controller.rb
skip_before_action :authenticate_user!

def index
  @teddies = Teddy.all
end

def show
  @teddy = Teddy.find(params[:id])
end

In the index handler we’re fetching all instances of Teddy and loading them into a template (which we will create shortly).

In show we’re reading the :id parameter (e.g. for the request GET /teddies/1 params[:id] would yield 1) as the argument to Teddy.find. This will return one Teddy or throw an exception if a Teddy with that id cannot be found.

By default, Rails will attempt to load a view for each action, using the following pattern:

app/view/:controller/:action.html.erb
# app/view/teddies/index.html.erb
# app/view/teddies/show.html.erb

Just one thing… those don’t exist yet! Create them with touch or with your text editor, starting with app/view/teddies/index.html.erb:

<div class="container">
  <div class="row">
    <h1>Teddies</h1>
    <% @teddies.each do |teddy| %>
      <div class="col-sm-6 col-md-4">
        <div class="thumbnail">
          <%= link_to image_tag(teddy.photo_url), teddy_path(teddy) %>
          <div class="caption">
            <h3><%= link_to teddy.name, teddy_path(teddy) %></h3>
            <p><%= teddy.category.name %></p>
            <p>$$$</p>
          </div>
        </div>
      </div>
    <% end %>
  </div>
</div>

Next, open your browser and head to http://localhost:3000/teddies.

If we click one of the <a> tags created in our template via:

<h3><%= link_to teddy.name, teddy_path(teddy) %></h3>

But, when our application passes GET /teddies/:id to TeddiesController#show we get an error, because we haven’t created the view yet:

ActionController::UnknownFormat (TeddiesController#show is missing a template for this request format and variant.

So, let’s overcome this error by creating app/view/teddies/index.html.erb:

<div class="container">
  <div class="row">
    <div class="col-sm-12">
      <h1><%= @teddy.name %></h1>
    </div>
  </div>
  <div class="row">
    <div class="col-sm-12 col-md-6">
      <%= image_tag(@teddy.photo_url, width: '100%') %>
    </div>
    <div class="col-sm-12 col-md-6">
      <p>Some awesome description of our amazing teddy.</p>
      <p>$$$</p>
    </div>
  </div>
</div>

Now, when we follow the link GET /teddies/:id our application responds with the rendered template!

5. Adding Prices

For an e-commerce site to make any sense, the products should have prices. Let’s add them.

First, in our gemfile we’ll add a new dependency: money-rails

# Gemfile
gem 'money-rails'

money-rails provides Rails an interface to ruby-money which will come in handy shortly.

Install the new dependencies:

bundle

money-rails provides an initialiser interface where we can configure how Money should work (e.g. setting the default currency.

Create config/initializers/money.rb:

# config/initializers/money.rb
MoneyRails.configure do |config|
  config.default_currency = :aud  # or :gbp, :usd, etc.
  config.locale_backend = nil
end 

Note: we’re going to add config.locale_backend = nil otherwise we’ll get a deprecation warning.

Now, let’s add a price column to our Teddies model. With rails generate run:

rails g migration AddPriceToTeddies

Open the newly created migration:

class AddPriceToTeddies < ActiveRecord::Migration[5.2]
  def change
  end
end

Add:

add_monetize :teddies, :price, currency: { present: false }

Then run rake db:migrate:

rake db:migrate
== 20190527032121 AddPriceToTeddies: migrating ================================
-- add_column(:teddies, "price_cents", :integer, {:null=>false, :default=>0})
   -> 0.0034s
== 20190527032121 AddPriceToTeddies: migrated (0.0035s) =======================

Finally, enable money on our Teddy model by opening app/models/teddy.rb and adding:

class Teddy < ApplicationRecord
  ...
  monetize :price_cents
end

Finally, let’s update our seeds file, so that our Teddies don’t cost NULL cents.

Teddy.create!(price: 100, ...)

Next, lets re-load our test data:

rake db:seed
Cleaning database...
Creating categories...
Creating teddies...
Finished!

We can check the data using pry… or we could just say a little prayer and hope it works.

Something to note about money and monetize is that setting the price field as 100 would yield the stored value 10000 cents. So yeah, just beware of that.

Finally, we’re ready to update our views to show the prices.

In app/views/teddies/index.html.erb add:

<p>Amount: <%= humanized_money_with_symbol(teddy.price) %></p>

And in app/views/teddies/index.html.erb add:

<p>Amount: <%= humanized_money_with_symbol(@teddy.price) %></p>

Voila.

6. Setting up Stripe

Sign up for an account with Stripe and retrieve your test API keys.

Next, install the Stripe ruby gem by adding it to your gemfile:

gem 'stripe'

And run:

bundle install
...
Fetching stripe 4.18.0
Installing stripe 4.18.0
...
Bundle complete!

Now let’s configure the Stripe initialiser by creating config/initializers/stripe.rb and adding:

Rails.configuration.stripe = {
  publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
  secret_key:      ENV['STRIPE_SECRET_KEY']
}

Stripe.api_key = Rails.configuration.stripe[:secret_key]

Provide those configuration values by creating a .env file at the root of your application:

STRIPE_PUBLISHABLE_KEY=<Publishable>
STRIPE_SECRET_KEY=<Secret>

7. Creating the Order Model

Great. Next we’ll create the Order model which belongs the User, allowing them to make purchases and provide us payment with Stripe.

Create the Order model with rails generate:

rails generate model Order state:string teddy_sku:string amount:monetize payment:jsonb user:references

Open the new migration:

class CreateOrders < ActiveRecord::Migration[5.2]
  def change
    create_table :orders do |t|
      t.string :state
      t.string :teddy_sku
      t.monetize :amount
      t.jsonb :payment
      t.references :user, foreign_key: true

      t.timestamps
    end
  end
end

Alter the amount field, removing currency, like so:

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

Run the migration:

rake db:migrate
== 20190527042058 CreateOrders: migrating =====================================
-- create_table(:orders)
   -> 0.0215s
== 20190527042058 CreateOrders: migrated (0.0216s) ============================

Enable monetize on the Order model. Open app/models/order.rb and add:

monetize :amount_cents

Finally, add the user -> has_many -> orders relationship to the User model. Open app/models/user.rb and add:

has_many :orders

Now, our model layer is complete.

8. Creating Orders

Finally, we have to create a view for the Order model, where the user can review their purchase, enter payment details, and initiate the actual payment.

Lets create an OrderController that handles GET /order/:id and POST /orders:

rails g controller orders

Next, using the resources method plug GET /order:/id into OrderController#show and POST /orders into OrderController#new. Open config/routes.rb and add:

resources :orders, only: [:show, :create] 

Next, we will update the Teddies#show template so that the User can purchase the teddy. In app/views/teddies/show.html.erb add:

<%= form_tag orders_path do %>
  <%= hidden_field_tag 'teddy_id', @teddy.id %>
  <%= submit_tag 'Purchase', class: 'btn btn-primary' %>
<% end %>

Now, if we go to GET /teddies/:id and click the new “Purchase” button, the browser will create a POST /orders request, and the router will invokeOrdersController#create:

The action 'create' could not be found for OrdersController

Let’s implement OrdersController#create. Open app/controllers/orders_controller.rb and add:

teddy = Teddy.find(params[:teddy_id])
order  = Order.create!(teddy_sku: teddy.sku, amount: teddy.price, state: 'pending', user: current_user)

redirect_to new_order_payment_path(order)

This creates a new Order, associated with the Teddy yielded by Teddy.find(params[:teddy_id], and the current User. It also sets the Order.status to pending.

All that’s left is to accept the payment and change the Order.status to completed.

9. Accepting Payments

Our application will handle requests to POST /orders/:order_id/payments  by creating and executing a Stripe payment.

Let’s create this handler with rails generate:

rails generate controller payments

In our new controller, let’s plug GET /orders/:order_id/payments/new  into PaymentsController#new and POST /orders/:order_id/payments  into PaymentsController#create.

In config/routes.rb add:

resources :orders, only: [:show, :create] do
  resources :payments, only: [:new, :create]
end

Nesting resources :payments within resources :orders ensures that our new :payments routes are prefixed by /order/:order_id/.

Next, let’s implement the new handlers in app/controllers/payments_controller.rb:

class PaymentsController < ApplicationController
  before_action :set_order

  def new
  end

  def create
    # ...
  end

private

  def set_order
    @order = current_user.orders.where(state: 'pending').find(params[:order_id])
  end
end

When a user pays with Stripe, it should be payed against a particular Order… preferably the one they’ve just created!

Using before_action, we define a method that runs just before PaymentsController#new and PaymentsController#create are invoked by the router.

before_action invokes PaymentsController#set_order, a private method that retrieves the last so-called “pending” Order from the current_user:

@order = current_user.orders.where(state: 'pending').find(params[:order_id])

Next, we’ll edit the view for PaymentsController#new by creating app/views/payments/new.html.erb:

<h1>Purchase of teddy <%= @order.teddy_sku %></h1>
<%= form_tag order_payments_path(@order) do %>
  <article>
    <label class="amount">
      <span>Amount: <%= humanized_money_with_symbol(@order.amount) %></span>
    </label>
  </article>

<script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
    data-key="<%= Rails.configuration.stripe[:publishable_key] %>"
    data-name="My Teddy"
    data-email="<%= current_user.email %>"
    data-description="Teddy <%= @order.teddy_sku %>"
    data-amount="<%= @order.amount_cents %>"
    data-currency="<%= @order.amount.currency %>"></script>
  -->
<% end %>

Sidebar: Test User

Unless there was something I missed, we have a problem.

PaymentsController#set_order invokes current_user but at this point, there is no current user - we have to create one. Open db/seeds.rb and add:

User.create! :email => 'teddies4eva@gmail.com', :password => 'obsessedwithteddies', :password_confirmation => 'obsessedwithteddies'

Now run rake db:seed again:

rake db:seed
Cleaning database...
Creating categories...
Creating teddies...
Creating user...
Finished!

We can now we can sign in at /users/sign_in. Once you’ve done that, current_user will yield a User which is a requirement for creating an Order in OrdersController#create.

This might be a good time to click through the whole checkout experience. You should get as far as GET /orders/:order_id/payments/new, which eventually loads app/views/payments/new.html.erb with our Stripe payment form:

<script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
    data-key="<%= Rails.configuration.stripe[:publishable_key] %>"
    data-name="My Teddy"
    data-email="<%= current_user.email %>"
    data-description="Teddy <%= @order.teddy_sku %>"
    data-amount="<%= @order.amount_cents %>"
    data-currency="<%= @order.amount.currency %>"></script>
  -->
<% end %>

Just a note on this. We are loading a Javascript file from Stripes own servers (i.e. https://checkout.stripe.com/checkout.js) and providing it a few bits of configuration:

  • The value for data-key is yielded from :

     Rails.configuration.stripe[:publishable_key]

    This value found originally in our .env file, was loaded into the global Rails configuration using the Stripe initialiser at: config/initialisers/stripe.rb.

    • For data-email the current_user’s email (e.g. teddies4eva@gmail.com.

    • For data-description we have @order.teddy_sku

    • For data-amount we have @order.amount_cents

    • And for data-currency it’s @order.amount.currency

checkout.js uses this configuration to create a <form> element which posts it’s payment-related form-data to POST /some-thing/, which is actually responsible for creating the payment.

We need to handle this request, so open app/controllers/payments_controller.rb and add the following to PaymentsController:

def create
    customer = Stripe::Customer.create(
      source: params[:stripeToken],
      email:  params[:stripeEmail]
    )

    charge = Stripe::Charge.create(
      customer:     customer.id,   # You should store this customer id and re-use it.
      amount:       @order.amount_cents,
      description:  "Payment for teddy #{@order.teddy_sku} for order #{@order.id}",
      currency:     @order.amount.currency
    )

    @order.update(payment: charge.to_json, state: 'paid')
    redirect_to order_path(@order)

  rescue Stripe::CardError => e
    flash[:alert] = e.message
    redirect_to new_order_payment_path(@order)
  end

This does a bunch of stuff!

First, it creates a Stripe::Customer from two of the values that were just sent via. checkout.js as form-data. Then it creates a Stripe::Charge which is a blocking function that sends data to Stripes’s servers, triggering the actual payment. Almost there! We update the Order using information from resulting charge and change the Order.state to: paid.

Next, we’ll re-direct the user to the newly completed order:

redirect_to order_path(@order)

Ok, cool. Just one problem! OrdersController#show doesn’t do anything yet. Open app/controllers/orders_controller.rb and add:

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

Now, the controller will load the Order with id yielded from /orders/:id.

Literally the last step is to create app/views/orders/show.html.erb:

<h1>Order</h1>
<ul>
  <li>Status: <%= @order.state %></li>
  <li>Item: <%= @order.teddy_sku %></li>
  <li>Amount: <%= humanized_money_with_symbol(@order.amount) %></li>
<ul>

Creating Test Payments

Use the card number:

4242 4242 4242 4242

Along with any valid date in the future for the expiration date.

And any random CCV code.

More cards: https://stripe.com/docs/testing#cards

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