何かサービスを作る際に、決済機能を導入したいはずです。
今回はPAY.JPを利用した決済機能を案内していきます。
PAY.JPの導入準備
スクリプトの記述
PAY.JPを使うためのスクリプトを記述します。
下記をコピーしてください。
%script{src: "https://js.pay.jp/", type: "text/javascript"}
コピーしたら、application.html.hamlに貼り付けましょう!!
%html
%head
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
%title payjptest
%script{src: "https://js.pay.jp/", type: "text/javascript"}
-# このscriptを記載
= csrf_meta_tags
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
%body
= yield
PAY.JPの登録
PAY.JPのアカウントを作成しましょう!
https://pay.jp/
APIキーを取得します
アカウントを作成してログインし、下記の場所のAPIキーを確認しましょう!
Appにgem 'payjp'を追加
# PAY.JPのgem
gem 'payjp'
# 環境変数を簡単に定義できるENVファイルを対応させるgem
gem 'dotenv-rails'
追加したら
$ bundle install
$ rails s
再起動しないとgemもscriptも読み込まれないので、エラーがおきます。
環境変数を利用して、APIキーをAppに登録
gem 'dotenv-rails'をインストールできたので、.envファイルを作成しましょう
app > .env の場所に作成します。
.gitignoreの上だと思えば簡単です。
秘密鍵をGithubにコミットしてしまうとAPIキーを世に公開してしまうので、.gitignoreに.envを記述します
/.env
ここ本当に重要なので、注意してくださいね!
では、APIキーを記述します
PAYJP_PRIVATE_KEY = 'sk_test_111111111111111111111111'
PAYJP_KEY = 'pk_test_111111111111111111111111'
記載したら、Githubのコミットに表示されていないか?確認します。
コミットに表示されてたら、ヤバイです。
危険です。.gitignoreを再確認してください。
ここまでで準備完了です。
素材作成(view):ここは自身で記述すると良いかと思います。
ここはviewですが、必要なければ飛ばしてください。
クレジットカードを登録するためのviewを作成します。
下記はformの部分テンプレートですが下記のような
スクリプト
#{asset_path 'creditcards/master-card.svg'}
master-card.svgの画像素材がないとエラーがおきますので、
コピペするなら素材を集めてください。
form
= form_with url:creditcards_path, method: :post, html: { name: "inputForm" },class:"form" do |f|
.form__upper
.form__upper__group
= f.label :カード番号
%span.form-require 必須
= f.text_field :card_number, name: "card_number", id:"card_number", type: "text", placeholder: '半角数字のみ', class: 'input-default', maxlength: "16"
%ol
%li
%object{type: "image/svg+xml", data: "#{asset_path 'creditcards/visa.svg'}", width:"35", height:"20"}
%li
%object{type: "image/svg+xml", data: "#{asset_path 'creditcards/master-card.svg'}", width:"35", height:"20"}
%li
%object{type: "image/svg+xml", data: "#{asset_path 'creditcards/saison-card.svg'}", width:"35", height:"20"}
%li
%object{type: "image/svg+xml", data: "#{asset_path 'creditcards/jcb.svg'}", width:"35", height:"20"}
%li
%object{type: "image/svg+xml", data: "#{asset_path 'creditcards/american_express.svg'}", width:"35", height:"20", class:"american_express"}
%li
%object{type: "image/svg+xml", data: "#{asset_path 'creditcards/dinersclub.svg'}", width:"35", height:"20"}
%li
%object{type: "image/svg+xml", data: "#{asset_path 'creditcards/discover.svg'}", width:"35", height:"20"}
.form__upper__group.exp
.name
= f.label :有効期限
%span.form-require 必須
= f.select :exp_year, [["19",2019],["20",2020],["21",2021],["22",2022],["23",2023],["24",2024],["25",2025],["26",2026],["27",2027],["28",2028],["29",2029]], {}, class: 'input-default harf', name: "exp_year", id:"exp_year"
%span 年
= f.select :exp_month, [["01",1],["02",2],["03",3],["04",4],["05",5],["06",6],["07",7],["08",8],["09",9],["10",10],["11",11],["12",12]],{}, class: 'input-default harf', name: "exp_month", id:"exp_month"
%span 月
.form__upper__group
= f.label :セキュリティーコード
%span.form-require 必須
= f.text_field :cvc, name: "cvc", id:"cvc", class:"cvc", type: "text", placeholder: 'カード背面4桁もしくは3桁の番号', class: 'input-default', maxlength: "16"
.form__upper__group
%p.about
= fa_icon 'question-circle'
%span
= link_to 'カード裏面の番号とは?', root_path, class:'about__registered'
.form__upper__group
= f.submit '次へ進む', class: 'btn-default', id: "charge-form"
data: "#{asset_path 'creditcards/visa.svg'}"は、
【 app > asset > images > creditcards > visa.svg 】を読み込む
という設定になります。
.form{
&__upper{
margin: 0 auto;
max-width: 343px;
p {
text-align: center;
}
&__group {
font-size: 14px;
color: #333;
&:not( :first-child ){
margin-top: 32px;
}
label{
font-weight: 600;
}
ol{
display:flex;
li {
margin: 5px 8px 0 0;
}
}
.form-require {
background-color: $green;
color: #fff;
font-size: 12px;
margin: 0 0 0 8px;
padding: 2px 4px;
border-radius: 2px;
vertical-align:top;
&-optional {
background-color: gray;
color: #fff;
font-size: 12px;
margin: 0 0 0 8px;
padding: 2px 4px;
border-radius: 2px;
vertical-align:top;
}
}
.input-default{
width: 90%;
margin: 8px 0 0;
height: 48px;
padding: 10px 16px 8px;
border-radius: 4px;
border: 1px solid #ccc;
background: #fff;
line-height: 1.5;
font-size: 16px;
&.harf{
width: calc(40% - 6px);
margin: 8px 8px 0 0;
height: 45px;
border-radius: 4px;
border: 1px solid #ccc;
background: #fff;
line-height: 1.5;
font-size: 16px;
&.exp{
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
}
&-select{
width: 76px;
margin-top: 8px;
height: 45px;
border-radius: 4px;
border: 1px solid #ccc;
background: #fff;
line-height: 1.5;
font-size: 16px;
}
}
h3 {
font-size: 16px;
font-weight: bold;
}
.attention{
margin: 8px 0 0;
}
.agree{
text-align: center;
}
a {
color: #0099e8;
text-decoration: none;
}
span {
margin: 0 2px;
}
.about{
text-align: right;
&__registered{
color: #0099e8;
text-decoration: none;
}
.fa-chevron-right {
color: #0099e8;
}
.fa-question-circle {
color: #0099e8;
font-size: 1rem
}
}
}
.form-info-text {
color: #888;
margin-top: 8px;
font-size: 14px;
}
}
&__bottom {
margin: 0 auto;
max-width: 343px;
.registance {
text-align: right;
}
}
.btn-default {
width: 100%;
height: 50px;
background-color: $green;
color: #FFFFFF;
font-size: 15px;
cursor: pointer;
}
.btn-registration{
color: #fff;
border-radius: 4px;
width: 50%;
line-height: 48px;
border: 1px solid transparent;
text-align: center;
margin: 0 auto;
position: relative;
i{
font-size:20px;
position: absolute;
top:13.5px;
left:15px;
}
.about__registered {
color: #FFFFFF;
text-decoration: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
&.email{
background-color: $green;
}
&.facebook{
background-color:#385184;
}
&.google{
background-color:#FFFFFF;
color: black;
background: #fff image-url('google.svg')
no-repeat 3px top;
border: #979797 solid 1px;
}
&:not( :first-child ){
margin-top: 16px;
}
}
}
= render 'shared/main-header'
.container-fluid
.row.py-5.w-100
= render 'shared/mypage-side'
.mypage-main.col-9
%h2.header_title
支払い方法
.single-container
.rgs-main__section
= render "shared/creditcard-form"
.payment-explain
= fa_icon 'chevron-right', class:"arrows"
= link_to '支払い方法について', '#'
モデル作成
$ rails g model creditcard
creditcardsテーブル
Column | Type | Options |
---|---|---|
user_id | references | foreign_key: true, null: false |
payjp_id | string | null: false |
Association
- belongs_to :user
ということでマイグレーションファイルは下記になります。
class CreateCreditcards < ActiveRecord::Migration[5.2]
def change
create_table :creditcards do |t|
t.references :user, foreign_key: true, null: false
t.string :payjp_id, null: false
t.timestamps
end
end
end
ここは他の記事と異なります。
- user_id: AppのUser-ID
- payjp_id: PAYJPのUser-ID
他の記事だとカード用のカラムも作成していますが、PAYJPのアカウントから引っ張りだせばいいので不要です。
本題のjQueryです。
$(document).on('turbolinks:load',function(){
// PAY.JPの公開鍵をセットします。
Payjp.setPublicKey('pk_test_111111111111111111');
//formのsubmitを止めるために, クレジットカード登録のformを定義します。
var form = $(".form");
$("#charge-form").click(function() {
// submitが完了する前に、formを止めます。
form.find("input[type=submit]").prop("disabled", true);
// submitを止められたので、PAY.JPの登録に必要な処理をします。
// formで入力された、カード情報を取得します。
var card = {
number: $("#card_number").val(),
cvc: $("#cvc").val(),
exp_month: $("#exp_month").val(),
exp_year: $("#exp_year").val(),
};
// PAYJPに登録するためのトークン作成
Payjp.createToken(card, function(status, response) {
if (response.error){
// エラーがある場合処理しない。
form.find('.payment-errors').text(response.error.message);
form.find('button').prop('disabled', false);
}
else {
// エラーなく問題なく進めた場合
// formで取得したカード情報を削除して、Appにカード情報を残さない。
$("#card_number").removeAttr("name");
$("#cvc").removeAttr("name");
$("#exp_month").removeAttr("name");
$("#exp_year").removeAttr("name");
var token = response.id;
form.append($('<input type="hidden" name="payjpToken" />').val(token));
form.get(0).submit();
};
});
});
});
コントローラーの記述
コントローラー(new&create)の作成
class CreditcardsController < ApplicationController
require "payjp"
before_action :set_card
def new
# cardがすでに登録済みの場合、indexのページに戻します。
@card = Creditcard.where(user_id: current_user.id).first
redirect_to action: "index" if @card.present?
end
def create
# PAY.JPの秘密鍵をセット(環境変数)
Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
# jsで作成したpayjpTokenがちゃんと入っているか?
if params['payjpToken'].blank?
# トークンが空なら戻す
render "new"
else
# トークンがちゃんとあれば進めて、PAY.JPに登録されるユーザーを作成します。
customer = Payjp::Customer.create(
description: 'test',
email: current_user.email,
card: params['payjpToken'],
metadata: {user_id: current_user.id}
)
# PAY.JPのユーザーが作成できたので、creditcardモデルを登録します。
@card = Creditcard.new(user_id: current_user.id, payjp_id: customer.id)
if @card.save
redirect_to action: "index", notice:"支払い情報の登録が完了しました"
else
render 'new'
end
end
end
private
def set_card
@card = Creditcard.where(user_id: current_user.id).first if Creditcard.where(user_id: current_user.id).present?
end
end
コントローラーの追記(index、destory)
class CreditcardsController < ApplicationController
require "payjp"
before_action :set_card
def index
# すでにクレジットカードが登録しているか?
if @card.present?
# 登録している場合,PAY.JPからカード情報を取得する
# PAY.JPの秘密鍵をセットする。
Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
# PAY.JPから顧客情報を取得する。
customer = Payjp::Customer.retrieve(@card.payjp_id)
# PAY.JPの顧客情報から、デフォルトで使うクレジットカードを取得する。
@card_info = customer.cards.retrieve(customer.default_card)
# クレジットカード情報から表示させたい情報を定義する。
# クレジットカードの画像を表示するために、カード会社を取得
@card_brand = @card_info.brand
# クレジットカードの有効期限を取得
@exp_month = @card_info.exp_month.to_s
@exp_year = @card_info.exp_year.to_s.slice(2,3)
# クレジットカード会社を取得したので、カード会社の画像をviewに表示させるため、ファイルを指定する。
case @card_brand
when "Visa"
@card_image = "visa.svg"
when "JCB"
@card_image = "jcb.svg"
when "MasterCard"
@card_image = "master-card.svg"
when "American Express"
@card_image = "american_express.svg"
when "Diners Club"
@card_image = "dinersclub.svg"
when "Discover"
@card_image = "discover.svg"
end
end
end
def new
@card = Creditcard.where(user_id: current_user.id).first
redirect_to action: "index" if @card.present?
end
def create
Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
if params['payjpToken'].blank?
render "new"
else
customer = Payjp::Customer.create(
description: 'test',
email: current_user.email,
card: params['payjpToken'],
metadata: {user_id: current_user.id}
)
@card = Creditcard.new(user_id: current_user.id, payjp_id: customer.id)
if @card.save
if request.referer&.include?("/registrations/step5")
redirect_to controller: 'registrations', action: "step6"
else
redirect_to action: "index", notice:"支払い情報の登録が完了しました"
end
else
render 'new'
end
end
end
def destroy
# 今回はクレジットカードを削除するだけでなく、PAY.JPの顧客情報も削除する。これによりcreateメソッドが複雑にならない。
# PAY.JPの秘密鍵をセットして、PAY.JPから情報をする。
Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
# PAY.JPの顧客情報を取得
customer = Payjp::Customer.retrieve(@card.payjp_id)
customer.delete # PAY.JPの顧客情報を削除
if @card.destroy # App上でもクレジットカードを削除
redirect_to action: "index", notice: "削除しました"
else
redirect_to action: "index", alert: "削除できませんでした"
end
end
private
def set_card
@card = Creditcard.where(user_id: current_user.id).first if Creditcard.where(user_id: current_user.id).present?
end
end
登録したカード情報を表示
.mypage-main.col-9
%h2.header_title
支払い方法
.single-container
%section.creditcard_section
%h3 クレジットカード一覧
- if @card.present?
.container
.creditcard-info
= image_tag "creditcards/#{@card_image}",width:'34',height:'20', alt:'master-card'
%p.creditcard-info__number
= "**** **** **** " + @card_info.last4 #クレジットカードの下4桁を表示
%p.creditcard-info__period
= @exp_month + " / " + @exp_year
= button_to "削除する", creditcard_path(@card), method: :delete, class:"creditcard-info__delete"
- else
.new-card
= link_to new_creditcard_path, class:"new-card-btn" do
%i.far.fa-credit-card
クレジットカードを追加する
購入処理を追加
def buy
@product = Product.find(params[:product_id])
# すでに購入されていないか?
if @product.buyer.present?
redirect_back(fallback_location: root_path)
elsif @card.blank?
# カード情報がなければ、買えないから戻す
redirect_to action: "new"
flash[:alert] = '購入にはクレジットカード登録が必要です'
else
# 購入者もいないし、クレジットカードもあるし、決済処理に移行
Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
# 請求を発行
Payjp::Charge.create(
amount: @product.price,
customer: @card.customer_id,
currency: 'jpy',
)
# 売り切れなので、productの情報をアップデートして売り切れにします。
if @product.update(buyer_id: current_user.id)
flash[:notice] = '購入しました。'
redirect_to controller: 'products', action: 'show', id: @product.id
else
flash[:alert] = '購入に失敗しました。'
redirect_to controller: 'products', action: 'show', id: @product.id
end
end
end
あとはボタンを押したら、buyアクションが動くようにすれば、完了です!!
以上です
お疲れ様です。
参考リンク
新規登録時にクレジットカード登録
- ユーザー登録と同時に生成
- [コントローラーでrender先を変える]
(https://qiita.com/taka_571/items/9b1c82d8fcc602df8a1a)
購入処理
- [buyページにProduct idを持っていく]
(https://qiita.com/hirokihello/items/fa82863ab10a3052d2ff) - 購入処理 公式
- 購入処理
- @product.idをどうやってもっていくか?