はじめに
本 Chapter では、顧客が注文履歴を閲覧できる機能を実装していきます。
閲覧機能のみですので、カート機能や決済処理の実装と比較すると難易度は下がります。
本アプリケーションの完成が見えてきました。一緒に頑張りましょう 💪
コントローラを作成しよう
コンテナが立ち上がっていることが確認できましたら、ecommerce_web コンテナに入ります。
その後、下記コマンドを実行して、必要なコントローラを作成しましょう。
$ rails g controller customer/orders index show success --no-helper
success アクション は、決済処理が成功した画面に対応するアクションです。
続いて、ルーティングの設定を行います。
config/routes.rb
を開いて下記コードを追加してください。
Rails.application.routes.draw do
devise_for :admins, controllers: {
sessions: 'admin/sessions'
}
devise_for :customers, controllers: {
sessions: 'customer/sessions',
registrations: 'customer/registrations'
}
root to: 'pages#home'
namespace :admin do
resources :products, only: %i[index show new create edit update]
end
scope module: :customer do
resources :products, only: %i[index show]
resources :cart_items, only: %i[index create destroy] do
member do
patch 'increase'
patch 'decrease'
end
end
resources :checkouts, only: [:create]
resources :webhooks, only: [:create]
+ resources :orders, only: %i[index show] do
+ collection do
+ get 'success'
+ end
+ end
end
get '/up/', to: 'up#index', as: :up
get '/up/databases', to: 'up#databases', as: :up_databases
end
さて、collection do
という記述が登場しました。
こちらは、生成されるパスに :id
が付与されません。すなわち、orders/success
というパスになるということです。(member do
には :id
が付与されましたね。)
生成されたパスを確認してみましょう。
$ rails routes | grep orders
success_orders GET /orders/success(.:format) customer/orders#success
orders GET /orders(.:format) customer/orders#index
order GET /orders/:id(.:format) customer/orders#show
/orders/success
には、:id
が付与されていませんね。
では次に、コントローラの各アクションの中身を記述していきます。
app/controllers/customer/orders_controller.rb
を開いて下記コードを記述してください。
class Customer::OrdersController < ApplicationController
before_action :authenticate_customer!
def index
@orders = current_customer.orders.latest
end
def show
@order = current_customer.orders.find(params[:id])
end
def success; end
end
index アクションでは、current_customer
に結びついている order を最新のものから順に取得しています。latest
メソッドは、app/models/application_record.rb
で定義しましたね。
show アクションでは、current_customer
に結びついている order から、id の情報をもとに一件取得しています。
success アクションをここで定義しましたので、app/controllers/customer/checkouts_controller.rb
の success_url
を修正しましょう。
class Customer::CheckoutsController < ApplicationController
before_action :authenticate_customer!
def create
line_items = current_customer.line_items_checkout
session = create_session(line_items)
# Allow redirection to the host that is different to the current host
redirect_to session.url, allow_other_host: true
end
private
def create_session(line_items)
Stripe::Checkout::Session.create(
client_reference_id: current_customer.id,
customer_email: current_customer.email,
mode: 'payment',
payment_method_types: ['card'],
line_items:,
shipping_address_collection: {
allowed_countries: ['JP']
},
shipping_options: [
{
shipping_rate_data: {
type: 'fixed_amount',
fixed_amount: {
amount: POSTAGE,
currency: 'jpy'
},
display_name: '全国一律'
}
}
],
- success_url: root_url,
+ success_url: "#{root_url}orders/success",
cancel_url: "#{root_url}carts"
)
end
end
これで、決済が完了した後、注文成功画面にリダイレクトされるようになります。
また、コントローラではありませんが、タイムゾーンを日本時間に変更しておきましょう。config/application.rb
を開いてください。下記のように、config.time_zone = 'Tokyo'
を追加すれば OK です。
require_relative 'boot'
require 'rails/all'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Hello
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
# Log to STDOUT because Docker expects all processes to log here. You could
# then collect logs using journald, syslog or forward them somewhere else.
logger = ActiveSupport::Logger.new($stdout)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
# Set Redis as the back-end for the cache.
config.cache_store = :redis_cache_store, {
url: ENV.fetch('REDIS_URL') { 'redis://redis:6379/1' },
namespace: 'cache'
}
# Set Sidekiq as the back-end for Active Job.
config.active_job.queue_adapter = :sidekiq
# Mount Action Cable outside the main process or domain.
config.action_cable.mount_path = nil
config.action_cable.url = ENV.fetch('ACTION_CABLE_FRONTEND_URL') { 'ws://localhost:28080' }
# Only allow connections to Action Cable from these domains.
origins = ENV.fetch('ACTION_CABLE_ALLOWED_REQUEST_ORIGINS') { "http:\/\/localhost*" }.split(',')
origins.map! { |url| /#{url}/ }
config.action_cable.allowed_request_origins = origins
# Customizing Rails Generators
config.generators do |g|
g.assets false
g.skip_routes true
g.test_framework false
end
+ config.time_zone = 'Tokyo'
end
end
注文日時をビューで表示するため、ここでタイムゾーンを変更しておきました。
タイムゾーンの変更を反映するために、コンテナを再起動してください。
ビューを作成しよう
それでは、最後に Tailwind CSS を使ってビューを作成していきましょう。
まずは、注文履歴の一覧画面です。app/views/customer/orders/index.html.erb
を開いてください。
<div class="mb-6 text-center">
<span class="text-3xl font-bold">
Order Histories
</span>
</div>
<% if @orders.count == 0 %>
<div class='mx-auto max-w-3xl'>
<p class='mb-8 text-xl text-center'>No history</p>
<div class='flex justify-center'>
<%= link_to products_path, class: 'group inline-flex items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-purple-600 to-blue-500 p-0.5 font-medium text-gray-900 hover:text-white focus:ring-4 focus:ring-blue-300 group-hover:from-purple-600 group-hover:to-blue-500' do %>
<span class='rounded-md bg-white px-5 py-2.5 transition-all duration-75 ease-in group-hover:bg-opacity-0'>
Find Products
</span>
<% end %>
</div>
</div>
<% else %>
<div class="w-full px-3">
<div class="shadow-soft-xl rounded-2xl bg-clip-border w-full lg:w-3/5 flex mx-auto">
<div class="w-full">
<ul class="rounded-lg">
<% @orders.each do |order| %>
<%= link_to order_path(order) do %>
<li class="py-7 px-6 mb-4 rounded-t-inherit rounded-xl bg-gray-50">
<div class="mb-5">
<p class="mb-4 leading-normal text-xl"><%= l order.created_at, format: :short %></p>
<p class="text-slate-700 inline-block font-bold text-center uppercase align-middle transition-all text-lg">
<%= number_to_currency(order.billing_amount, unit: "¥", strip_insignificant_zeros: true) %>
</p>
<span class="ml-2 bg-blue-100 text-blue-800 text-xs font-semibold p-2 rounded">
<%= order.status %>
</span>
</div>
<div>
<p class="mb-2 leading-tight text-sm">Name: <span class="font-semibold text-slate-700 sm:ml-2"><%= order.name %></span></p>
<p class="mb-2 leading-tight text-sm">Postal Code: <span class="font-semibold text-slate-700 sm:ml-2"><%= order.postal_code %></span></p>
<p class="mb-2 leading-tight text-sm">Prefecture: <span class="font-semibold text-slate-700 sm:ml-2"><%= order.prefecture %></span></p>
<p class="mb-2 leading-tight text-sm">Address: <span class="font-semibold text-slate-700 sm:ml-2"><%= order.address1 %> <%= order.address2 %></span></p>
</div>
</li>
<% end %>
<% end %>
</ul>
</div>
</div>
</div>
<% end %>
注文日時の表示には、l
メソッドを採用しています。
ビューの記述が完了しましたら、http://localhost:8000/orders にアクセスして、注文履歴一覧が表示されるか確認してみましょう。
上記のように、注文履歴が最新のものから順に表示されていれば OK です。
それでは、次に注文履歴の詳細画面を作成しましょう。app/views/customer/orders/show.html.erb
を開いて、下記コードを記述してください。
<div class="mb-8 text-center">
<span class="text-3xl font-bold">
Order Placed
</span>
<br />
<span class="text-xl font-bold">
<%= l @order.created_at, format: :short %>
</span>
</div>
<div class="px-6">
<div class="flex flex-col xl:flex-row jusitfy-center w-full xl:space-x-8 space-y-4 md:space-y-6 xl:space-y-0">
<div class="flex flex-col justify-start items-start w-full space-y-4 md:space-y-6 xl:space-y-8">
<div class="flex flex-col justify-start items-start bg-gray-50 px-4 py-4 md:py-6 md:p-6 xl:p-8 w-full">
<p class="text-lg md:text-xl font-semibold leading-6 xl:leading-5 text-gray-800">Order Details</p>
<% @order.order_details.each do |order_detail| %>
<%= link_to product_path(order_detail.product) do %>
<div class="mt-4 md:mt-6 flex flex-row justify-start items-start md:items-center space-x-6 xl:space-x-8 w-full">
<div class="pb-4 md:pb-8 w-40">
<%= image_tag order_detail.product.image, class: "aspect-square w-full rounded-xl object-cover" %>
</div>
<div class="border-b border-gray-200 md:flex-row flex-col flex justify-between items-start w-full pb-8 space-y-4 md:space-y-0">
<div class="w-full flex flex-col justify-start items-start space-y-8">
<p class="text-3xl font-semibold leading-6 text-gray-800"><%= order_detail.product.name %></p>
<div class="flex justify-start items-start flex-col space-y-2">
<p class="text-lg leading-none text-gray-800"><%= number_to_currency(order_detail.price, unit: "¥", strip_insignificant_zeros: true) %></p>
<p class="text-md leading-none text-gray-800">Qty: <%= order_detail.quantity %></p>
</div>
</div>
</div>
</div>
<% end %>
<% end %>
</div>
</div>
<div class="flex flex-col justify-start items-start w-full space-y-4 md:space-y-6 xl:space-y-8">
<div class="flex justify-center flex-col md:flex-row items-stretch w-full space-y-4 md:space-y-0 md:space-x-6 xl:space-x-8">
<div class="flex flex-col px-4 py-6 md:p-6 xl:p-8 w-full bg-gray-50 space-y-6">
<h3 class="text-xl font-semibold leading-5 text-gray-800">Summary</h3>
<div class="flex justify-center items-center w-full space-y-4 flex-col border-gray-200 border-b pb-4">
<div class="flex justify-between w-full">
<p class="text-base leading-4 text-gray-800">Subtotal</p>
<p class="text-base leading-4 text-gray-600"><%= number_to_currency(@order.billing_amount - @order.postage, unit: "¥", strip_insignificant_zeros: true) %></p>
</div>
<div class="flex justify-between items-center w-full">
<p class="text-base leading-4 text-gray-800">Shipping</p>
<p class="text-base leading-4 text-gray-600"><%= number_to_currency(@order.postage, unit: "¥", strip_insignificant_zeros: true) %></p>
</div>
</div>
<div class="flex justify-between items-center w-full">
<p class="text-base font-semibold leading-4 text-gray-800">Total</p>
<p class="text-base font-semibold leading-4 text-gray-600"><%= number_to_currency(@order.billing_amount, unit: "¥", strip_insignificant_zeros: true) %></p>
</div>
</div>
</div>
<div class="bg-gray-50 w-full h-fit px-4 py-6 md:p-6 xl:p-8 ">
<h3 class="text-xl font-semibold leading-5 text-gray-800">Shipping Address</h3>
<div class="flex flex-col md:flex-row xl:flex-col justify-start w-full md:space-x-6 lg:space-x-8 xl:space-x-0">
<div class="flex justify-between w-full flex-col mt-6">
<div class="flex justify-center md:justify-start xl:flex-col flex-col md:space-x-6 lg:space-x-8 xl:space-x-0 space-y-4 xl:space-y-12 md:space-y-0 md:flex-row items-center md:items-start">
<div class="flex justify-center md:justify-start items-center md:items-start flex-col space-y-4">
<div class="w-full xl:w-48 text-center md:text-left text-sm leading-5 text-gray-600">
<p class="mb-2 leading-tight text-sm">Name: <span class="font-semibold text-slate-700 sm:ml-2"><%= @order.name %></span></p>
<p class="mb-2 leading-tight text-sm">Postal Code: <span class="font-semibold text-slate-700 sm:ml-2"><%= @order.postal_code %></span></p>
<p class="mb-2 leading-tight text-sm">Prefecture: <span class="font-semibold text-slate-700 sm:ml-2"><%= @order.prefecture %></span></p>
<p class="mb-2 leading-tight text-sm whitespace-nowrap">Address1: <span class="font-semibold text-slate-700 sm:ml-2"><%= @order.address1 %></span></p>
<% if @order.address2 %>
<p class="mb-2 leading-tight text-sm whitespace-nowrap">Address2: <span class="font-semibold text-slate-700 sm:ml-2"><%= @order.address2 %></span></p>
<% end %>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-center flex-col md:flex-row items-stretch w-full space-y-4 md:space-y-0 md:space-x-6 xl:space-x-8">
<div class="px-4 py-6 md:p-6 xl:p-8 w-full bg-gray-50 space-y-6">
<h3 class="text-xl font-semibold leading-5 text-gray-800">Order Status</h3>
<p>Latest Status: <span class="mt-5 bg-blue-100 text-blue-800 text-md font-semibold py-2 px-2.5 rounded"><%= @order.status %></span></p>
</div>
</div>
</div>
</div>
</div>
ビューの記述が完了しましたら、http://localhost:8000/orders/1 にアクセスして、注文履歴の詳細が表示されるか確認してみましょう。id には適切な order の id を指定してください。
上記のような画面が表示されれば OK です。
では最後に、注文完了画面を作成しましょう。app/views/customer/orders/success.html.erb
を開いて、下記コードを記述してください。
<div class="bg-white p-6 md:mx-auto">
<svg viewBox="0 0 24 24" class="text-green-600 w-16 h-16 mx-auto my-6">
<path fill="currentColor" d="M12,0A12,12,0,1,0,24,12,12.014,12.014,0,0,0,12,0Zm6.927,8.2-6.845,9.289a1.011,1.011,0,0,1-1.43.188L5.764,13.769a1,1,0,1,1,1.25-1.562l4.076,3.261,6.227-8.451A1,1,0,1,1,18.927,8.2Z">
</path>
</svg>
<div class="text-center">
<p class="md:text-2xl text-base text-gray-900 font-semibold text-center">Payment Done</p>
<p class="text-gray-600 my-2">Thank you for coming to our shop.</p>
<div class="py-8 text-center">
<%= link_to orders_path, class: "group inline-flex items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-purple-600 to-blue-500 p-0.5 font-medium text-gray-900 hover:text-white focus:ring-4 focus:ring-blue-300 group-hover:from-purple-600 group-hover:to-blue-500" do %>
<span class='rounded-md bg-white px-5 py-2.5 transition-all duration-75 ease-in group-hover:bg-opacity-0'>
Go order history
</span>
<% end %>
</div>
</div>
</div>
ビューの記述が完了しましたら、もう一度注文を行ってみましょう。
決済が完了して下記のような画面が表示されれば OK です。
最後に、ナビゲーションバーに注文履歴一覧画面へのリンクを追加します。app/views/layouts/application.html.erb
を開いて、「Account settings」リンクの下に追加しましょう。
<div class="py-1">
<%= link_to edit_customer_registration_path, class: "text-gray-700 flex justify-between w-full px-4 py-2 text-sm leading-5 text-left" do %>
Account settings
<% end %>
<%= link_to orders_path, class: "text-gray-700 flex justify-between w-full px-4 py-2 text-sm leading-5 text-left" do %>
Order Histories
<% end %>
</div>
下記のように「Order histories」というリンクが追加されていれば OK です。
以上で顧客側の注文に関する機能は全て実装し終わりました 🎉
最後に、RuboCop を実行し、必要であればコードを修正しましょう。
$ rubocop
今回は、RuboCop の静的解析を全て PASS したようです。
Inspecting 58 files
..........................................................
58 files inspected, no offenses detected
それでは、コミットしておきましょう。
$ git add . && git commit -m "Implementation on the customer's side of the order history"
おわりに
お疲れ様でした。
本 Chapter では、顧客が注文履歴を閲覧する機能を実装しました。
次の Chapter にて、同じような機能を管理者側で実装していきます。