【第六章】フリマアプリ開発完全ロードマップ【商品購入機能】

はじめに

本記事は第五章 商品カテゴリ機能のつづきです。
Railsを用いてフリマアプリの根幹機能である商品の購入機能を実装します。
本章がこのロードマップの最終章となります。

フリマアプリ開発完全ロードマップ

内容 難易度 所要時間 主要技術
序 章 AWS自動デプロイ ★☆☆☆☆ ★★★☆☆ capistrano
第一章 データベース設計 ★☆☆☆☆ ★☆☆☆☆
第二章 マークアップ作業 ★★☆☆☆ ★★★★★
第三章 ユーザ登録/ログイン機能 ★★★★☆ ★★☆☆☆ devise
第四章 商品出品/編集/詳細表示/削除 ★★★★☆ ★★★☆☆ carrierwave
第五章 商品カテゴリ機能 ★★★★★ ★★★☆☆ ancestry
第六章 商品購入機能 ★★★★★ ★★★★☆ Payjp

記事を読むにあたっての前提条件

この記事には私が作成した大量のコードが載っていますが、
コードレビューのための記事ではありませんので、「コードが汚い!」などのコメントはご遠慮願います。
リファクタリングについては、編集リクエストで私に直接お知らせ頂ければ幸いです。
ご理解のほど、何卒よろしくお願いいたします。

1.gem"payjp"の導入

今回の購入機能実装ではクレジットカードを登録するためのpayjpというgemを使います。

Gemfile
gem'payjp'
ターミナル(ローカル)
bundle install

また、payjpの利用にはpayjpのアカウント登録が必須なので登録しておいて下さい。
https://pay.jp/

2.cardsコントローラおよびモデルの作成

購入機能の実装にあたり、新たにcardsコントローラおよびCardモデルを作成します。

ターミナル(ローカル)
rails g controller cards
rails g model card
20200502000000_create_cards.rb
class CreateCards < ActiveRecord::Migration[5.2]
  def change
    create_table :cards do |t|
      t.references :user       , null: false, foreign_key: true
      t.string     :card_id    , null: false
      t.string     :customer_id, null: false
      t.timestamps
    end
  end
end
ターミナル(ローカル)
rails db:migrate

続いて、既存モデルとの紐付けをしておきます。

card.rb
belongs_to :user
user.rb
has_one :card

3.カード登録/照会画面および商品購入確認/完了画面を作成

以下のビューが必要になるので、新たに作成します。
 ・商品購入確認画面
 ・商品購入完了画面
 ・カード登録画面
 ・カード照会画面

まずはそれぞれに対応したルーティングを定義しましょう。

routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
  }
  devise_scope :user do
    get  'addresses', to: 'users/registrations#new_address'
    post 'addresses', to: 'users/registrations#create_address'
  end
  $date = Time.now.in_time_zone('Tokyo').to_s
  root "products#index"
  resources :products do
    collection do
      get "set_images"
      get "set_parents"
      get "set_children"
      get "set_grandchildren"
    end
    # 編集箇所ここから
    member do
      get "buy"
      get "pay"
    end
    # 編集箇所ここまで
  end
  # 編集箇所ここから
  resources :cards, only: [:new, :create, :show, :destroy] do
    collection do
      post 'show', to: 'card#show'
    end
  end
  # 編集箇所ここまで
  resources :users, only: :show
end

ビュー表示用にコントローラも以下のように編集しておきます。

cards_controller.rb
class CardsController < ApplicationController

  def new
    @card = Card.new
  end

  def show
    @default_card_information = {number: "4242424242424242", exp_month: "12", exp_year: "2020"}
  end
end

showビューの表示にダミーデータが必要なので登録しておきます。

スクリーンショット 2020-05-02 19.29.56.png

そして、ルーティングに対応するビューを作成します。

カード登録画面

スクリーンショット 2020-05-03 11.45.34.png

cards/new.html.haml
- contents = ["マイページ"             ,"いいね一覧","出品する"        ,"出品した商品","購入した商品","評価一覧","発送元・お届け先変更","支払い方法","ログアウト"               ]
- links    = [user_path(current_user),""         ,new_product_path,""          ,""          ,""       ,""                ,""         ,destroy_user_session_path]
- method   = ["get"                  ,""         ,"get"           ,""          ,""          ,""       ,""                ,""         ,"delete"                ]

= render "layouts/header"
.show-users-wrapper
  .show-users-wrapper__center
    .show-users-wrapper__center__left-content
      - contents.each.with_index(0) do |content,i|
        = link_to links[i], method: method[i],class: "show-users-wrapper__center__left-content__box" do
          = content
    = form_with model: @card, url: cards_path do |f|
      .show-users-wrapper__center__right-content
        .card-wrapper
          .card-wrapper__title
            クレジットカード情報登録
          .card-wrapper__content-title
            カード番号
            %span{class:"require"}
              必須
          = f.text_field :number, {class: "card-wrapper__input-text", placeholder: "ハイフンなし、半角数字のみ", value:"4242424242424242"}
          .card-wrapper__content-title
            有効期限
            %span{class:"require"}
              必須
          - month = [["月",""]]
          - for num in 1..12
            - month << num
          = f.select :exp_month, month, {}, {class: "card-wrapper__input-text input-half"}
          - year = [["年",""]]
          - for num in 0..20
            - year << 2040-num
          = f.select :exp_year, year, {}, {class: "card-wrapper__input-text input-half"}
          .card-wrapper__content-title
            セキュリティーコード
            %span{class:"require"}
              必須
          = f.text_field :cvc, {class: "card-wrapper__input-text", placeholder: "3〜4文字、半角数字のみ"}
          = f.submit "登録する", {class: "card-wrapper__input-text submit-btn"}
= render "layouts/footer"
application.scss
@import "cards";
cards.scss
.card-wrapper{
  padding: 20px;
  height: 457px;
  &__title{
    text-align: center;
    font-weight: bold;
  }
  &__content-title{
    margin-top: 20px;
    .require{
      padding: 3px;
      color: white;
      font-size: 12px;
      border-radius: 4px;
      background-color: $frema;
    }
  }
  &__input-text{
    height: 44px;
    line-height: 44px;
    font-size: 16px;
    padding: 0 16px;
    width: 460px;
    margin-top: 10px;
    border-radius: 4px;
  }
  .input-half{
    width: 217px;
    margin-right: 10px;
  }
  .submit-btn{
    color: white;
    margin-top: 30px;
    border-radius: 0px;
    background-color: $frema;
    border: 0px solid #000;
  }
  .disabled{
    background-color: #f0f0f0;
  }
  img{
    margin-top: 20px;
    margin-left: 130px;
    width: 200px;
  }
}

カード照会画面

スクリーンショット 2020-05-03 12.34.15.png

cards/show.html.haml
- contents = ["マイページ"             ,"いいね一覧","出品する"        ,"出品した商品","購入した商品","評価一覧","発送元・お届け先変更","支払い方法","ログアウト"               ]
- links    = [user_path(current_user),""         ,new_product_path,""          ,""          ,""       ,""                ,""         ,destroy_user_session_path]
- method   = ["get"                  ,""         ,"get"           ,""          ,""          ,""       ,""                ,""         ,"delete"                ]

= render "layouts/header"
.show-users-wrapper
  .show-users-wrapper__center
    .show-users-wrapper__center__left-content
      - contents.each.with_index(0) do |content,i|
        = link_to links[i], method: method[i],class: "show-users-wrapper__center__left-content__box" do
          = content
    .show-users-wrapper__center__right-content
      .card-wrapper
        .card-wrapper__title
          クレジットカード情報照会
        = image_tag "visa-card.png"
        .card-wrapper__content-title
          カード番号
        %input{type: "text", class: "card-wrapper__input-text disabled", disabled: true, value: "**** **** **** " + "#{@default_card_information[:number].last(4)}"}
          .card-wrapper__content-title
            有効期限
          %input{type: "text", class: "card-wrapper__input-text input-half disabled", disabled: true, value: @default_card_information[:exp_month]}
          %input{type: "text", class: "card-wrapper__input-text input-half disabled", disabled: true, value: @default_card_information[:exp_year]}
= render "layouts/footer"

カード照会画面のCSSはカード登録画面と共通です。

商品購入確認画面

マークアップに入る前に、商品詳細画面から購入確認画面へリンクできるように設定しておきます。

products/show.html.haml
= link_to buy_product_path(@product), class:"show-wrapper__main__buy-button" do
  商品購入へ進む

products_controller.rbにbuyアクションを定義し、
カード表示用のダミー情報を与えておきます。

products_controller.rb
  # buyを追加
  before_action :set_product, only: [:show, :edit, :update, :destroy, :buy]

  def buy
    @default_card_information = {number: "4242424242424242", exp_month: "12", exp_year: "2020"}
  end

スクリーンショット 2020-05-03 16.04.47.png

以下、マークアップコードです。

buy.html.haml
= render "layouts/header"
.buy-wrapper
  .buy-wrapper__main
    .buy-wrapper__main__title
      .buy-wrapper__main__title__text.center
        購入内容の確認
    .border
    .buy-wrapper__main__contents
      = image_tag "#{@product.images[0].image}", class: "buy-wrapper__main__contents__left"
      .buy-wrapper__main__contents__right
        = @product.name
        %br
        = #{@product.price.to_s(:delimited)} (税込)"
        %br
        = @product.postage
    .border
    .buy-wrapper__main__title
      .buy-wrapper__main__title__text
        支払い金額
      .buy-wrapper__main__title__text
        = #{@product.price.to_s(:delimited)}"
    .buy-wrapper__main__title
      .buy-wrapper__main__title__text
        支払い方法
      - if Card.where(user: current_user).exists?
        .buy-wrapper__main__title__text.light.space
          = "**** **** **** " + "#{@default_card_information[:number].last(4)}"
          = image_tag "visa-card.png"
      - else
        = link_to new_card_path, class: "link-blue" do
          登録して下さい
    .buy-wrapper__main__title
      .buy-wrapper__main__title__text
        配送先
      .buy-wrapper__main__title__text.light.width
        = "〒#{Address.find_by(user: current_user).post_number}"
        %br
        = Address.find_by(user: current_user).prefecture
        = Address.find_by(user: current_user).city
        = Address.find_by(user: current_user).address
        - unless Address.find_by(user: current_user).apartment == nil
          = Address.find_by(user: current_user).apartment
    - if Card.where(user: current_user).exists?
      = link_to pay_product_path(@product), class: "buy-wrapper__main__pay-btn" do
        購入を確定する
= render "layouts/footer"
application.scss
@import "products_buy";
products_buy.scss
.buy-wrapper{
  background-color: #eee;
  padding: 40px 0;
  &__main{
    width: 720px;
    padding-bottom: 30px;
    background-color: white;
    margin: 0 auto;
    &__title{
      margin: 0 auto;
      font-size: 20px;
      font-weight: bold;
      padding: 20px 0px;
      width: 300px;
      display: flex;
      justify-content: space-between;
      .light{
        font-size: 14px;
        padding-top: 6px;
        position: relative;
        font-weight: normal;
      }
      .space{
        padding-right: 56px;
      }
      .width{
        width: 170px;
        font-size: 12px;
      }
      .center{
        margin: 0 auto;
      }
      img{
        position: absolute;
        top: 0;
        right: 0;
        height: 30px;
      }
    }
    &__contents{
      width: 300px;
      margin: 0 auto;
      padding: 20px 0;
      display: flex;
      align-items: center;
      justify-content: space-around;
      &__left{
        width: 140px;
        height: 100px;
        object-fit: cover;
      }
      &__right{
        width: 140px;
        font-size: 14px;
        line-height: 24px;
      }
    }
    &__pay-btn{
      display: block;
      margin: 0 auto;
      text-align: center;
      color: white;
      padding: 10px;
      width: 300px;
      background-color: $frema;
    }
    .border{
      border-bottom: 1px solid #eee;
    }
  }
}

商品購入完了画面

スクリーンショット 2020-05-03 16.06.21.png

pay.html.haml
= render "layouts/header"
.new-wrapper
  .new-wrapper__main
    .new-wrapper__main__create
      商品の購入が完了しました
      %br
      = link_to root_path,class: "link-blue" do
        トップページに戻る
= render "layouts/footer"

4.カード登録機能の実装

まずはpayjpの公開鍵・秘密鍵を設定していきます。

Payjpの公式ホームページにアクセスして、左のリストからAPIを選び、
テスト用秘密鍵テスト用公開鍵をコピーします。
スクリーンショット 2020-05-03 16.14.03.png
そして、ターミナルで以下を実行。

ターミナル(ローカル)
// OSがCatalina以降の方は、以下を実行して下さい
sudo vim ~/.zshrc

// OSがMojave以前の方は、以下を実行して下さい
sudo vim ~/.bash_profile

そして、payjpの公開鍵と秘密鍵を以下のように書き込みます。

export PAYJP_PRIVATE_KEY='sk_test_xxxxxxxxxxxxxxxxxxxxxxxx'
export PAYJP_PUBLIC_KEY='pk_test_xxxxxxxxxxxxxxxxxxxxxxxx'

編集の際は、iキーで編集モードに切り替えて編集、完了の際はescキーを2回押して:wqで上書き保存です。編集後に、以下を実行します。

// OSがCatalina以降の方は、以下を実行して下さい
source vim ~/.zshrc

// OSがMojave以前の方は、以下を実行して下さい
source vim ~/.bash_profile

これで準備完了。次はpayjpトークンの発行を行います。

トークンとは、1回だけ使える一時パスワードのようなものです。顧客に入力してもらったカードの情報をそのままデータベースに保存してしまうと、ハッキングされて情報を盗まれる恐れがあるので、payjpのセキュリティーガチガチサーバに保存しておいて、その情報をトークンで借りてくる、という形を取ります。

トークンの発行はカードの登録時と支払いの発生時に行います。
つまりcards#new cards#createproducts#buy products#payです。
まずはcards#new cards#create、つまりカード登録のアクションを定義していきます。

cards_controller.rb
  def new
    @card = Card.new
    card = Card.where(user_id: current_user.id)
    if card.exists?
      redirect_to card_path(card[0].id)
    end
  end

  def create
    Payjp.api_key = ENV['PAYJP_PRIVATE_KEY']
    token = Payjp::Token.create({
      card: {
        number:     params[:card][:number],
        cvc:        params[:card][:cvc],
        exp_month:  params[:card][:exp_month],
        exp_year:   params[:card][:exp_year]
      }},
      {'X-Payjp-Direct-Token-Generate': 'true'} 
    )
    if token.blank?
      redirect_to new_card_path
    else
      customer = Payjp::Customer.create(card: token)
      card = Card.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
      card.save!
      if card.save!
        redirect_to card_path(card)
      else
        redirect_to new_card_path
      end
    end
  end

token = Payjp::Token.createという記述でトークンを発行できます。
あとは、トークン発行に必要なparamsをビューで与えてあげればOKです。

cards/new.html.haml
- contents = ["マイページ"             ,"いいね一覧","出品する"        ,"出品した商品","購入した商品","評価一覧","発送元・お届け先変更","支払い方法","ログアウト"               ]
- links    = [user_path(current_user),""         ,new_product_path,""          ,""          ,""       ,""                ,""         ,destroy_user_session_path]
- method   = ["get"                  ,""         ,"get"           ,""          ,""          ,""       ,""                ,""         ,"delete"                ]

= render "layouts/header"
.show-users-wrapper
  .show-users-wrapper__center
    .show-users-wrapper__center__left-content
      - contents.each.with_index(0) do |content,i|
        = link_to links[i], method: method[i],class: "show-users-wrapper__center__left-content__box" do
          = content
    = form_with model: @card, url: cards_path do |f|
      .show-users-wrapper__center__right-content
        .card-wrapper
          .card-wrapper__title
            クレジットカード情報登録
          .card-wrapper__content-title
            カード番号
            %span{class:"require"}
              必須
          = f.text_field :number, {class: "card-wrapper__input-text", placeholder: "ハイフンなし、半角数字のみ", value:"4242424242424242"}
          .card-wrapper__content-title
            有効期限
            %span{class:"require"}
              必須
          - month = [["月",""]]
          - for num in 1..12
            - month << num
          = f.select :exp_month, month, {}, {class: "card-wrapper__input-text input-half"}
          - year = [["年",""]]
          - for num in 0..20
            - year << 2040-num
          = f.select :exp_year, year, {}, {class: "card-wrapper__input-text input-half"}
          .card-wrapper__content-title
            セキュリティーコード
            %span{class:"require"}
              必須
          = f.text_field :cvc, {class: "card-wrapper__input-text", placeholder: "3〜4文字、半角数字のみ"}
          = f.submit "登録する", {class: "card-wrapper__input-text submit-btn"}
= render "layouts/footer"

これでカード登録が可能となるので、カードを1つ登録しておいて下さい。

また、先ほど作成したshowおよびbuyのビューがダミーのままなので、
登録したカードの情報が表示できるように修正しておきます。

cards_controller.rb
  def show
    card = Card.find_by(user_id: current_user.id)
    if card.blank?
      redirect_to action: "create"
    else
    Payjp.api_key = ENV['PAYJP_PRIVATE_KEY']
    customer = Payjp::Customer.retrieve(card.customer_id)
    @default_card_information = Payjp::Customer.retrieve(card.customer_id).cards.data[0]
    end
  end
cards/show.html.haml
- contents = ["マイページ"             ,"いいね一覧","出品する"        ,"出品した商品","購入した商品","評価一覧","発送元・お届け先変更","支払い方法","ログアウト"               ]
- links    = [user_path(current_user),""         ,new_product_path,""          ,""          ,""       ,""                ,""         ,destroy_user_session_path]
- method   = ["get"                  ,""         ,"get"           ,""          ,""          ,""       ,""                ,""         ,"delete"                ]

= render "layouts/header"
.show-users-wrapper
  .show-users-wrapper__center
    .show-users-wrapper__center__left-content
      - contents.each.with_index(0) do |content,i|
        = link_to links[i], method: method[i],class: "show-users-wrapper__center__left-content__box" do
          = content
    .show-users-wrapper__center__right-content
      .card-wrapper
        .card-wrapper__title
          クレジットカード情報照会
        = image_tag "visa-card.png"
        .card-wrapper__content-title
          カード番号
        %input{type: "text", class: "card-wrapper__input-text disabled", disabled: true, value: "**** **** **** " + "#{@default_card_information.last4}"}
          .card-wrapper__content-title
            有効期限
          %input{type: "text", class: "card-wrapper__input-text input-half disabled", disabled: true, value: @default_card_information.exp_month}
          %input{type: "text", class: "card-wrapper__input-text input-half disabled", disabled: true, value: @default_card_information.exp_year}
= render "layouts/footer"
products_controller.rb
def buy
    unless @product.soldout
      card = Card.where(user_id: current_user.id)
      if card.exists?
        @card     = Card.find_by(user_id: current_user.id)
        Payjp.api_key = ENV['PAYJP_PRIVATE_KEY']
        customer = Payjp::Customer.retrieve(@card.customer_id)
        @default_card_information = Payjp::Customer.retrieve(@card.customer_id).cards.data[0]
      end
    else
      redirect_to product_path(@product)
    end
  end
buy.html.haml
= render "layouts/header"
.buy-wrapper
  .buy-wrapper__main
    .buy-wrapper__main__title
      .buy-wrapper__main__title__text.center
        購入内容の確認
    .border
    .buy-wrapper__main__contents
      = image_tag "#{@product.images[0].image}", class: "buy-wrapper__main__contents__left"
      .buy-wrapper__main__contents__right
        = @product.name
        %br
        = #{@product.price.to_s(:delimited)} (税込)"
        %br
        = @product.postage
    .border
    .buy-wrapper__main__title
      .buy-wrapper__main__title__text
        支払い金額
      .buy-wrapper__main__title__text
        = #{@product.price.to_s(:delimited)}"
    .buy-wrapper__main__title
      .buy-wrapper__main__title__text
        支払い方法
      - if Card.where(user: current_user).exists?
        .buy-wrapper__main__title__text.light.space
          -# 修正箇所ここから
          = "**** **** **** " + "#{@default_card_information.last4}"
          -# 修正箇所ここまで
          = image_tag "visa-card.png"
      - else
        = link_to new_card_path, class: "link-blue" do
          登録して下さい
    .buy-wrapper__main__title
      .buy-wrapper__main__title__text
        配送先
      .buy-wrapper__main__title__text.light.width
        = "〒#{Address.find_by(user: current_user).post_number}"
        %br
        = Address.find_by(user: current_user).prefecture
        = Address.find_by(user: current_user).city
        = Address.find_by(user: current_user).address
        - unless Address.find_by(user: current_user).apartment == nil
          = Address.find_by(user: current_user).apartment
    - if Card.where(user: current_user).exists?
      = link_to pay_product_path(@product), class: "buy-wrapper__main__pay-btn" do
        購入を確定する
= render "layouts/footer"

5.機能修正

購入機能の実装作業に入る前に、2点修正を加えておきます。
 1. 売り切れ商品を購入できなくする
 2. 自分で出品した商品を購入できないようにする

上記をまとめてやっていきます。
まずは売り切れ判定を行うために、productモデルにsoldoutカラムを追加します。

ターミナル(ローカル)
rails g migration add_column_product_soldout
20200503000000_add_column_product_soldout.rb
class AddColumnProductSoldout < ActiveRecord::Migration[5.2]
  def change
    add_column :products, :soldout, :boolean, null: false, default: false
  end
end
ターミナル(ローカル)
rails db:migrate

productsのindexとshowにそれぞれsoldout表示ができるようにしておきます。

products/index.html.haml
    .top-main__products
      - @products[0..4].each do |product|
        = link_to product_path(product.id) do
          .top-main__products__product
            -# 編集箇所ここから
            - if product.soldout == true
              .soldout
                SOLD OUT
            -# 編集箇所ここまで
            - product.images.each.with_index(1) do |image,i|
              - if i == 1
                = image_tag("#{image.image}")
            .top-main__products__product__price
              = #{product.price.to_s(:delimited)}"
            .top-main__products__product__text
              = product.name
products_indes.scss
.soldout{
  letter-spacing: 1px;
  width: 100%;
  position: absolute;
  font-size: 9px;
  text-align: center;
  padding: 2px;
  background-color: $frema;
  font-weight: bold;
  color: white;
}

下記のshow.html.hamlは修正が二箇所あります。

products/show.html.haml
= render "layouts/header"
.show-wrapper{action: request.path}
  .show-wrapper__main
    .show-wrapper__main__product-name
      = @product.name
      %br
      - if @product.user_id == current_user.id
        = link_to edit_product_path(@product), class:"link-blue", data: {turbolinks: false}  do
          商品を編集する
      - if @product.user_id == current_user.id
        = link_to product_path(@product), method: "delete", class:"link-blue" do
          商品を削除する
    .show-wrapper__main__contents
      .show-wrapper__main__contents__left
        -# 編集箇所ここから
        - if @product.soldout == true
          .soldout
            SOLD OUT
        -# 編集箇所ここまで
        - @product.images.each.with_index do |image,i|
          - if i == 0
            = image_tag "#{image.image}", class:"show-wrapper__main__contents__left__image"
        .show-wrapper__main__contents__left__icons
          - @product.images.each.with_index do |image,i|
            - if i == 0
              = image_tag "#{image.image}", class:"show-wrapper__main__contents__left__icons__icon", index: i
            - else
              = image_tag "#{image.image}", class:"show-wrapper__main__contents__left__icons__icon non-active", index: i
      .show-wrapper__main__contents__right
        .show-wrapper__main__contents__right__table
          .show-wrapper__main__contents__right__table__left
            出品者
          .show-wrapper__main__contents__right__table__right
            = User.find(@product.user_id).nickname
        .show-wrapper__main__contents__right__table
          .show-wrapper__main__contents__right__table__left
            カテゴリー
          .show-wrapper__main__contents__right__table__right
            = link_to "#", class: "link-blue" do
              = @product.category.parent.parent.name
              %br
              %i{class: "fas fa-chevron-right"}
              = @product.category.parent.name
              %br
              %i{class: "fas fa-chevron-right"}
              = @product.category.name
        .show-wrapper__main__contents__right__table
          .show-wrapper__main__contents__right__table__left
            ブランド
          .show-wrapper__main__contents__right__table__right
            - unless @product.brand.blank?
              = @product.brand
        .show-wrapper__main__contents__right__table
          .show-wrapper__main__contents__right__table__left
            商品のサイズ
          .show-wrapper__main__contents__right__table__right
            - unless @product.size.blank?
              = @product.size
        .show-wrapper__main__contents__right__table
          .show-wrapper__main__contents__right__table__left
            商品の状態
          .show-wrapper__main__contents__right__table__right
            = @product.condition
        .show-wrapper__main__contents__right__table
          .show-wrapper__main__contents__right__table__left
            配送料の負担
          .show-wrapper__main__contents__right__table__right
            = @product.postage
        .show-wrapper__main__contents__right__table
          .show-wrapper__main__contents__right__table__left
            配送の方法
          .show-wrapper__main__contents__right__table__right
            らくらくフリマ便
        .show-wrapper__main__contents__right__table
          .show-wrapper__main__contents__right__table__left
            配送元地域
          .show-wrapper__main__contents__right__table__right
            = @product.region
        .show-wrapper__main__contents__right__table
          .show-wrapper__main__contents__right__table__left
            発送日の目安
          .show-wrapper__main__contents__right__table__right
            = @product.shipping_days
    .show-wrapper__main__price
      %h1
        = #{@product.price.to_s(:delimited)}"
      %span{class: "show-wrapper__main__price__tax"} (税込)
      - if @product.postage == "送料込み(出品者負担)"
        送料込み
      - else
        送料別
    -# 編集箇所ここから
    - if @product.soldout == false && @product.user_id != current_user.id
      = link_to buy_product_path(@product), class:"show-wrapper__main__buy-button" do
        商品購入へ進む
    -# 編集箇所ここまで
    .show-wrapper__main__explanation
      = br(@product.explanation)
    .show-wrapper__main__bottom
      .show-wrapper__main__bottom__btn-wrapper
        .show-wrapper__main__bottom__btn-wrapper__btn
          %i{class: "far fa-heart"}
          いいね!
          0
        .show-wrapper__main__bottom__btn-wrapper__btn
          %i{class: "far fa-flag"}
          不適切な商品の報告
      = link_to "#", class:"show-wrapper__main__bottom__link link-blue" do
        %i{class: "fas fa-lock"}
        あんしん、あんぜんへの取り組み
  .show-wrapper__main
    .show-wrapper__main__comment-wrapper
      .show-wrapper__main__comment-wrapper__name
        令和の明智光秀
      .show-wrapper__main__comment-wrapper__icon
        = image_tag "user_icon.png"
      .show-wrapper__main__comment-wrapper__comment
        = image_tag "bubble.png"
        = br("こんにちは!\nこちら3000円にお値下げは可能でしょうか?")
        %br
        %i{class: "far fa-clock"} 3時間前
        %i{class: "far fa-flag"}
    .show-wrapper__main__comment-wrapper
      .show-wrapper__main__comment-wrapper__name
        令和の織田信長(本物)
      .show-wrapper__main__comment-wrapper__icon
        = image_tag "user_icon.png"
      .show-wrapper__main__comment-wrapper__comment
        = image_tag "bubble.png"
        = br("令和の明智光秀さん\nOKです!3000円に値下げしましたので、よろしくお願いします。")
        %br
        %i{class: "far fa-clock"} 3時間前
        %i{class: "far fa-flag"}
    .show-wrapper__main__comment-input
      .show-wrapper__main__comment-input__caution
        相手のことを考え丁寧なコメントを心がけましょう。不快な言葉遣いなどは利用制限や退会処分となることがあります。
      %textarea{class: "show-wrapper__main__comment-input__textfield"}
      %input{type: "submit", class: "show-wrapper__main__comment-input__submit", value: "コメントを投稿する"}
  .show-wrapper__main
    .show-wrapper__main__sns-icons
      %i{class: "fab fa-facebook-square"}
      %i{class: "fab fa-twitter"}
      %i{class: "fab fa-line"}
      %i{class: "fab fa-pinterest"}
  .show-wrapper__others
    = link_to "#", class: "show-wrapper__others__title link-blue" do
      = "令和の織田信長(本物)"
      さんのその他の出品
    .show-wrapper__others__products
      - for num in 1..6
        .show-wrapper__others__products__product
          = image_tag "1616794_s.jpg", class: "show-wrapper__others__products__product__image"
          .show-wrapper__others__products__product__bottom
            .show-wrapper__others__products__product__bottom__text
              商品サンプル商品サンプル商品サンプル商品サンプル商品サンプル商品サンプル商品サンプル
            .show-wrapper__others__products__product__bottom__price
              = #{1980.to_s(:delimited)}"
              %span{class: "show-wrapper__others__products__product__bottom__price__like"}
                %i{class: "far fa-heart"}
                0
= render "layouts/footer"
products_show.scss
.soldout{
  letter-spacing: 1px;
  width: 100%;
  position: absolute;
  text-align: center;
  padding: 2px;
  background-color: $frema;
  font-weight: bold;
  color: white;
}

6.購入機能の実装

最後に、購入機能の実装です。
いよいよ実装も大詰めです。頑張っていきましょう!

products_controller.rbにpayアクションを定義します。

products_controller.rb
  # payを追加
  before_action :set_product, only: [:show, :edit, :update, :destroy, :buy, :pay]

  def pay
    unless @product.soldout
      @card = Card.find_by(user_id: current_user.id)
      @product.soldout = true
      @product.save!
      Payjp.api_key = ENV['PAYJP_PRIVATE_KEY']
      @charge = Payjp::Charge.create(
      amount: @product.price,
      customer: @card.customer_id,
      currency: 'jpy'
      )
    else
      redirect_to product_path(@product)
    end
  end

Payjp::Charge.createという記述によって支払いが行われます。
あとは、作成した画面で商品を実際に購入してみましょう。
商品が売り切れ表示に変わり、Payjp公式サイトに売上が登録されれば実装完了です!
スクリーンショット 2020-05-03 21.25.34.png

お疲れ様でした!

これでフリマアプリの機能実装を行うことができました!
ここまで読んでいただき、本当にありがとうございました。
他にも検索機能やいいね機能など、実装できる機能は沢山ありますので、是非実装を進めてみて下さい。

内容 難易度 所要時間 主要技術
序 章 AWS自動デプロイ ★☆☆☆☆ ★★★☆☆ capistrano
第一章 データベース設計 ★☆☆☆☆ ★☆☆☆☆
第二章 マークアップ作業 ★★☆☆☆ ★★★★★
第三章 ユーザ登録/ログイン機能 ★★★★☆ ★★☆☆☆ devise
第四章 商品出品/編集/詳細表示/削除 ★★★★☆ ★★★☆☆ carrierwave
第五章 商品カテゴリ機能 ★★★★★ ★★★☆☆ ancestry
第六章 商品購入機能 ★★★★★ ★★★★☆ Payjp

あとがき

この記事のここをもっと解説してほしい!などありましたら、コメント欄でお伝えください。
また、コメント欄での暴言や誹謗中傷の書き込み等はご遠慮ください。マジで凹むので。

いつも記事を読んで頂いている皆さま、LGTMを押していただいている皆さま、本当にありがとうございます!!
有益な記事を執筆するモチベーションに繋がりますので、この記事が役に立ったなと思って頂けたら、
是非LGTMボタンのクリックSNSでのシェアをよろしくお願いします。


添野文哉(@enjoy_omame)
https://twitter.com/enjoy_omame

enjoy_omame
TECH CAMPというスクール出身です。 Rails系男子。hamlとjQueryが好きです。
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
コメント
この記事にコメントはありません。
あなたもコメントしてみませんか :)
すでにアカウントを持っている方は
ユーザーは見つかりませんでした