Skip to content

Instantly share code, notes, and snippets.

@rexmortus
Created May 27, 2019 03:42
Show Gist options
  • Save rexmortus/8fe5970be104fe3f09c7d4307433747b to your computer and use it in GitHub Desktop.
Save rexmortus/8fe5970be104fe3f09c7d4307433747b to your computer and use it in GitHub Desktop.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment