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.1 What does the product page need to do?
- Load an instance of
Teddy
from the database 2. Render a template showing:Teddy
imageTeddy
description- an action (i.e. button that
POST
s form-data, creating a newOrder
)
1.2 What does order summary page need to do?
- Load the newly-created instance of
Order
2. Render a template showing:Teddy
imageTeddy
descriptionTeddy
price- show the computed price information (tax, shipping, total)
- provide an action (i.e. button that
POST
s form-data to apayments controller
which creates a new Stripe payment, attaches the result to correspondingOrder
, and then redirects to the order, showing completion)
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.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.
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:
- teddies_controller.rb which implements
TeddiesController
: the handler forGET /teddies
andGET /teddies/<id>
. - An empty folder at
app/views/teddies
where the template files go - teddies_controller_test.rb which implements
TeddiesControllerTest
: which implements integration tests forTeddiesConstroller
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
intoTeddiesController#index
GET /teddies/:id
intoTeddiesController#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!
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.
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>
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.
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
.
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 %>
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
thecurrent_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>
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