Stripeコンビニ決済をRailsで導入してみる
こんにちは、株式会社スペースマーケットのエンジニアのtchmrです。
コンビニ決済(以下、コンビニ支払い)について検証したので知見を共有できればと思います。
コンビニで利用料を支払うことができればクレジットカードを持っていない方にもサービスを利用いただきやすくなるかと思っています。
公式ドキュメント
コンビニ支払いの流れ
コンビニ支払いがどのような流れで行われるのかをざっくり示すと以下となります。
スペースの予約時
- 利用者がコンビニ支払いを選択してスペースの予約をする
- 予約ステータスが支払い待ちとなるので、利用者はコンビニで支払いを行う
- Stripe側で支払いを受け付けた旨の情報がスペースマーケットに連携される
- 予約ステータスが予約完了に更新される
予約キャンセルに伴う返金時
- 管理者が予約キャンセルを行う
- 利用者宛にStripeから返金口座登録メールが送信される
- 利用者が返金口座を登録する
- Stripeから返金ステータス更新イベントがスペースマーケットに連携される
- Stripeと銀行間で送金処理が行われる
- Stripeから返金成功または失敗の返金ステータス更新イベントがスペースマーケットに連携される
- 予約ステータスが払い戻しに更新される
コンビニ支払いAPI
支払情報の作成
予約(購入)時にPaymentIntentを作成します。
PaymentIntentは支払いに関する情報を管理するためのオブジェクトで、Charge(課金情報)とTransfer(送金情報)が紐付いています。
直訳すると支払いの意思
となりあまりピンとこないですが、支払いに関する情報をまとめて管理している抽象的なオブジェクトなのだなくらいに認識しています。
以下、Rubyのサンプルリクエストとなります。
Stripe::PaymentIntent.create(
{
amount: 1000,
currency: 'jpy',
payment_method_types: ['konbini'],
payment_method_options: {
konbini: {
product_description: 'Tシャツ', # 商品情報
expires_after_days: 3, # コンビニで支払いを行うまでの有効期限
},
},
payment_method_data: {
type: 'konbini',
billing_details: {
name: '利用者名',
email: 'customer@examle.com', # 返金時の口座情報入力メールの宛先
},
},
on_behalf_of: 連結アカウントID, # 支払い記録対象※1
transfer_data: { # このプロパティを設定しておくとTrasferも同時作成される
amount: 1000,
destination: 連結アカウントID, # 送金先アカウント
},
}
)
※1 on_behalf_of
プロパティを指定することで支払いの記録対象をプラットフォームではなく、各連結アカウントにすることができます。コンビニ支払いではStripeによる審査が必要となりますが、この指定をする場合は連結アカウントごとにコンビニ支払いのケイパビリティを有効化する必要があります。
上記のリクエストを実行するとStripe上にPaymentIntentが作成されます。
ただし、この時点では利用者は支払いを完了していないため、ステータスがrequires_action(コンビニ支払い待ち)
となります。
利用者がコンビニ支払いを完了または有効期限が切れるとStripeはpayment_intent.succeeded/failed
イベントを送信します。
システム側でWebhookハンドラーを構築して連携されたWebhookを処理できるようにする必要があります。
こちらについては別記事を参照いただければと思います。
現実的には、PaymentIntentを作成する際にDB登録などの処理も行うことが多いと思われるためServiceクラスで処理をまとめたサンプルコードを用意しました。
class CreateKonbiniPaymentService
require "stripe"
attr_accessor :options
attr_accessor :stripe_service
# optionsにリクエストに必要な情報が含まれていると仮定
# stripe_serviceはテストしやすいようにmockを挿入できるようにしています
def self.call(options, stripe_service: Stripe::PaymentIntent)
new(options, stripe_service).call
end
def initialize(options, stripe_service)
self.options = options
self.stripe_service = stripe_service
end
# コンビニ支払い 課金作成処理
def call
set_stripe_api_setting
begin
response = create_payment_intent(generate_payment_intent_param)
result = JSON.parse(response.to_json)
rescue => e
Rails.logger.error("支払オブジェクト作成に失敗しました。#{e.message}")
raise e
end
begin
# DB登録
KonbiniPayment.create!(
payment_intent_id: result[:payment_intent_id],
payment_expires_at: result[:payment_expires_at],
charge_amount: result[:charge_amount],
voucher_url: result[:voucher_url] # コンビニ支払いコードを表示するためのURL。予約ページに表示したり、予約受付メールに記載して支払動線を作る際に使用します。
# ...省略
)
rescue => e
Rails.logger.error("支払情報のDB登録に失敗しました。#{e.message}")
# DB登録に失敗した場合はStripe上のPaymentIntentをロールバックする
payment_intent_id = result[:payment_intent_id]
payment_intent_obj = retrieve_payment_intent(payment_intent_id)
cancel_payment_intent(payment_intent_id) if payment_intent_obj.present?
Rails.logger.info("支払オブジェクトをキャンセルしました。")
raise e
end
result
end
private
def set_stripe_api_setting
Stripe.api_key = ENV["STRIPE_API_KEY"] # 環境変数に設定しておく
Stripe.api_version = "xxxx-xx-xx" # APIバージョンを指定
end
def generate_payment_intent_param
{
amount: options[:charge_amount],
currency: 'jpy',
payment_method_types: ['konbini'],
payment_method_options: {
konbini: {
product_description: options[:product_description],
expires_after_days: options[:expires_after_days]
},
},
payment_method_data: {
type: 'konbini',
billing_details: {
name: options[:customer_name],
email: options[:customer_email]
},
},
on_behalf_of: options[:connect_id],
transfer_data: {
amount: options[:transfer_amount],
destination: options[:connect_id],
}
}
end
# 以下、Stripe APIリクエスト処理
# エラーハンドリングできるようにメソッドを定義しています(実際はもっと丁寧なエラーハンドリングをしたほうが良いです)
def create_payment_intent(params)
stripe_service.create(params)
rescue => e
Rails.logger.error "Stripe API Error: #{e.message}"
raise e
end
def retrieve_payment_intent(payment_intent_id)
stripe_service.retrieve(payment_intent_id)
rescue => e
Rails.logger.error "Stripe API Error: #{e.message}"
raise e
end
def cancel_payment_intent(payment_intent_id)
stripe_service.cancel(payment_intent_id)
rescue => e
Rails.logger.error "Stripe API Error: #{e.message}"
raise e
end
end
予約時にCreateKonbiniPaymentService.call(options)
を呼び出してあげればStripeとDBに支払オブジェクトを作成することができます。
返金
コンビニ支払い後にキャンセルとなる場合などは返金を行う必要があります。
コンビニ支払いでは、利用者が登録した銀行口座に返金額が入金される形となります。
Stripe::Refund.create(
{
payment_intent: PaymentIntentID,
amount: 1000, # 返金額
reverse_transfer: true, # Transferの返金も併せて作成※2
}
)
※2 reverse_transfer
プロパティをtrueとすることでPaymentIntentに紐づくTransferについても同額の差し戻しを実行します。
金額が相違する場合は別途Transferの差し戻しを行う必要があります。
上記のリクエストを実行するとRefundオブジェクトが作成されるとともに、自動的にStripeから返金口座登録メールが利用者のメールアドレス宛に送信されます。
※ 利用書のメールアドレスはPaymentIntentを作成した際に登録したpayment_method_data.billing_details.email
です。
口座情報を入力するとStripeから銀行口座への送金処理が行われます。(実際に利用者の口座に入金されるまでにはある程度の日数がかかります)
返金オブジェクトのステータスが変更される際にはcharge.refund.updated
イベントが送信されるのでPaymentIntentの作成時と同様にWebhookハンドラを構築することで追跡することが可能です。
その他
StripeはAPIが充実しているため様々な操作ができます。
詳しくはAPIドキュメントをご覧ください。
感想
決済手段の多様化に伴ってサービスに複数の決済手段を導入する必要性が高まってきています。
Spacemarketでは、クレジットカード以外にもAmazonPayや楽天ペイ、Paidy、Paidなどを導入しています。
決済手段を追加することはビジネスインパクトがある一方でプロダクトコードが複雑化してしまうリスクもあります。
キャッシュレス決済が進展している現在の状況からして今後も決済手段が増加していく可能性は高く、そのことを見越した設計・リファクタリングをしていく必要があると感じました。
最後に
スペースマーケットでは一緒に働いてくれる仲間を探しています。
少しでも興味を持っていただけたらぜひ採用ページをご覧ください。
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion